飞道的博客

Spring全家桶怎么这么多bug?

212人阅读  评论(0)

理解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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场