理解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方法:
within(feign.Client+) point execution(Response org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient.execute(Request,Options)), args:[GET http://client/feignaop/server HTTP/1.1
Binary data, feign.Request$Options@68a7f77a]
一开始这个项目使用的是客户端的负载均衡,即 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的确切成功了:
@within(org.springframework.cloud.openfeign.FeignClient) pjp execution(String spring.demo4.feign.ClientWithUrl.api()), args:[]
但这次切入的是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模块注释之后,是不是可以解决问题呢?
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
但,问题并没解决,启动出错误了:
Caused by: java.lang.IllegalArgumentException: Cannot subclass final class feign.httpclient.ApacheHttpClient
at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:657)
at org.springframework.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
这是为啥呢?
首先,你知道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是否可以切入。
转载:https://blog.csdn.net/qq_33589510/article/details/116755207