新鲜出炉!阿里 P8 大牛整理的 Spring 框架宝典限时开源,全网首发
今日分享开始啦,请大家多多指教~
Spring 全家桶怎么有这么多 bug?今天就给大家分享一下。
理解 Spring 的体系结构和使用方式有一定曲线,Spring 多年发展堆积起来的内部结构非常复杂。Spring 框架内部的复杂度主要源于:
Spring 框架借助 IoC 和 AOP 的功能,实现了修改、拦截 Bean 的定义和实例的灵活性,因此真正执行的代码流程并不是串行的
Spring Boot 根据当前依赖情况实现了自动配置,虽然省去了手动配置的麻烦,但也因此多了一些黑盒、提升了复杂度
Spring Cloud 模块多版本也多,Spring Boot 1.x 和 2.x 的区别也很大。如果要对 Spring Cloud 或 Spring Boot 进行二次开发的话,考虑兼容性的成本会很高。
Feign AOP 切不到
我曾遇到过这么一个案例:使用 Spring Cloud 做微服务调用,为方便统一处理 Feign,想到了用 AOP 实现,即使用 within 指示器匹配 feign.Client 接口的实现进行 AOP 切入。
代码如下,通过 @Before 在执行方法前打印日志,并在代码中定义为 @FeignClient 的 Client 类,让其成为一个 Feign 接口:
通过 Feign 调用服务后可以看到日志中有输出,的确实现了 feign.Client 的切入,切入的是 execute 方法:
一开始这个项目使用的是客户端的负载均衡,即 Ribbon,代码没啥问题。后来因为后端服务通过 Nginx 实现服务端负载均衡,所以开发同学把 @FeignClient 的配置设置了 URL 属性,直接通过一个固定 URL 调用后端服务:
但这样配置后,之前的 AOP 切面竟然失效了,即 within(feign.Client+)无法切入 ClientWithUrl 的调用。
bug 场景复现:
调用 Client 后 AOP 有日志输出,调用 ClientWithUrl 后却没有。这就令人费解了。难道为 Feign 指定了 URL,其实现就不是 feign.Client 了?
看来,还是要看源码 - FeignClient 的创建过程,即 FeignClientFactoryBean#getTarget 方法。
259 行,if 判断,当 URL 没有内容,即为空或者不配置时调用 loadBalance 方法,在其内部通过 FeignContext 从容器获取 feign.Client 的实例:
调试可知,client 是 LoadBalanceFeignClient,是经过代理增强的 Bean
实践可得:
未指定 URL 的 @FeignClient 对应的 LoadBalanceFeignClient,可以通过 feign.Client 切入
URL 非空,client 设为 LoadBalanceFeignClient 的 delegate 属性。因为有了 URL 就不需要客户端负载均衡了,但因为 Ribbon 在 classpath 中,所以需要从 LoadBalanceFeignClient 提取出真正的 Client。client 是个 ApacheHttpClient
这个 ApacheHttpClient 从哪里来的呢?
如果你希望知道一个类是怎样调用栈初始化的,可以在构造方法中设置一个断点进行调试。这样,你就可以在 IDE 的栈窗口看到整个方法调用栈,然后点击每一个栈帧看到整个过程。
用这种方式,我们可以看到,是 HttpClientFeignLoadBalancedConfiguration 类实例化的 ApacheHttpClient:
进一步查看 HttpClientFeignLoadBalancedConfiguration#LoadBalancerFeignClient
所以 ApacheHttpClient 是 new 出来的,并不是 Bean,而 LoadBalancerFeignClient 是一个 Bean。
所以 within(feign.Client+)无法切入设置过 URL 的 @FeignClient ClientWithUrl:
表达式声明的是切入 feign.Client 的实现类
Spring 只能切入由自己管理的 Bean
虽然 LoadBalancerFeignClient 和 ApacheHttpClient 都是 feign.Client 接口的实现,但 HttpClientFeignLoadBalancedConfiguration 的自动配置只是把前者定义为 Bean,后者是 new 出来的、作为了 LoadBalancerFeignClient 的 delegate,不是 Bean
在定义了 FeignClient 的 URL 属性后,我们获取的是 LoadBalancerFeignClient 的 delegate,它不是 Bean
因此,定义了 URL 的 FeignClient 采用 within(feign.Client+)无法切入。
那如何解决呢?
修改一下切点表达式,通过 @FeignClient 注解来切:
修改后通过日志看到,AOP 的确切成功了:
但这次切入的是 ClientWithUrl 接口的 API 方法,并不是 client.Feign 接口的 execute 方法,显然不符合预期。
这是因为没有弄清楚真正希望切的是什么对象。
@FeignClient 标记在 Feign Client 接口,所以切的是 Feign 定义的接口,即每一个实际的 API 接口。而通过 feign.Client 接口切的是客户端实现类,切到的是通用的、执行所有 Feign 调用的 execute 方法。
那 ApacheHttpClient 不是 Bean 无法切入,且 Feign 接口本身又不符合要求。怎么办?
ApacheHttpClient 其实有机会独立成为 Bean。
查看 HttpClientFeignConfiguration 源码,当没有 ILoadBalancer 类型时,自动装配会把 ApacheHttpClient 设置为 Bean。
这么做的原因很明确,如果不希望做客户端负载均衡,应该不会引用 Ribbon 组件的依赖,自然没有 LoadBalancerFeignClient,只有 ApacheHttpClient:
那,把 pom.xml 中的 ribbon 模块注释之后,是不是可以解决问题呢?
但,问题并没解决,启动出错误了:
这是为啥呢?
首先,你知道 Spring 动态代理有哪些方式吗?
JDK 动态代理
通过反射实现,只支持对实现接口的类进行代理
CGLIB 动态字节码注入方式
通过继承实现代理,无上述限制
Spring Boot 2.x 默认使用 CGLIB 代理,但通过继承实现代理有个问题,无法继承 final 类。因为,ApacheHttpClient 类就是 final。
为解决该问题,把配置参数 proxy-target-class 的值修改为 false
修改后执行 clientWithUrl 接口可以看到,通过 within(feign.Client+)方式可以切入 feign.Client 子类了。
所以 Spring Cloud 使用了自动装配来根据依赖装配组件,组件是否成为 Bean 决定了 AOP 是否可以切入。
今日份分享已结束,请大家多多包涵和指点!
评论