ToB企服应用市场:ToB评测及商务社交产业平台

标题: 动态代理类注册为Spring Bean的坑 [打印本页]

作者: 数据人与超自然意识    时间: 2022-10-17 03:04
标题: 动态代理类注册为Spring Bean的坑
背景介绍:

最近在搭建一个公共项目,类似业务操作记录上报的功能,就想着给业务方提供统一的sdk,在sdk中实现客户端和服务端的交互封装,对业务方几乎是无感的。访问关系如下图:

访问关系示意图
这里采用了http的方式进行交互,但是,如果每次接口调用都需要感知http的封装,一来代码重复度较高,二来新增或修改接口也需要同步更改客户端代码,就有点不太友好,维护成本较高;能否实现像调用本地方法一样调用远程服务(RPC)呢,当然是可以的,并且也有好多可以参考的例子。例如,feign client的实现思路,定义好服务端的接口,通过Java代理的方式创建代理类,在代理类中统一封装了http的调用,并且将代理类作为一个bean注入到Spring容器中,使用的时候就只要获取bean调用相应的方法即可。
写个简单的例子来验证一下:

假设有个远程服务,提供了如下接口:
  1. package com.example.remoteserviceproxydemo;
  2. /**
  3. * IRemoteService
  4. * @author beetle_shu
  5. */
  6. public interface IRemoteService {
  7.     /**
  8.      * getGreetingName
  9.      * @return
  10.      */
  11.     String getGreetingName();
  12.     /**
  13.      * sayHello
  14.      * @param name
  15.      * @return
  16.      */
  17.     String sayHello(String name);
  18. }
复制代码
接下来,我们自定义一个InvocationHandler 来实现远程方法的调用
  1. package com.example.remoteserviceproxydemo;
  2. import java.lang.reflect.InvocationHandler;
  3. import java.lang.reflect.Method;
  4. /**
  5. * RemoteServiceInvocationHandler
  6. * @author beetle_shu
  7. */
  8. public class RemoteServiceInvocationHandler implements InvocationHandler {
  9.     @Override
  10.     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  11.         // 如果是远程http服务调用,通常有以下几步:
  12.         // 1. 解析方法和参数:可以通过自定义注解,在方法上定义远程服务地址,请求方式GET/POST等信息
  13.         // 2. 采用httpclient,OkHttp,或者restTemplate进行远程服务调用
  14.         // 3. 解析http响应,反序列化成对应接口方法的返回对象
  15.         // 这里,我们就不真正调用服务了,伪代码只是验证下被调用的方法是不是我们自己定义的,
  16.         // 如果是的话返回当前方法名,如果不是的话,抛出异常,程序中断
  17.         checkMethod(method);
  18.         String methodName = method.getName();
  19.         String param = "";
  20.         if (args != null && args.length > 0) {
  21.             param = String.valueOf(args[0]);
  22.         }
  23.         return methodName + ":" + param;
  24.     }
  25.     private void checkMethod(Method method) {
  26.         Method[] methods = IRemoteService.class.getDeclaredMethods();
  27.         for (Method m : methods) {
  28.             if (m.getName().equals(method.getName())) {
  29.                 return;
  30.             }
  31.         }
  32.         throw new RuntimeException("method which is not declared, " + method.getName());
  33.     }
  34. }
复制代码
紧接着,通过java.lang.reflect.Proxy代理类创建一个代理对象,代理远程服务的调用,同时把该对象注册为Spring bean,加入Spring容器
  1. package com.example.remoteserviceproxydemo;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import java.lang.reflect.Proxy;
  5. @Configuration
  6. public class RemoteServiceProxyDemoConfiguration {
  7.     @Bean
  8.     public IRemoteService getRemoteService() {
  9.         return (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),
  10.                 new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());
  11.     }
  12. }
复制代码
最后,我们创建一个Controller来调用测试一下:
  1. package com.example.remoteserviceproxydemo;
  2. import org.springframework.web.bind.annotation.GetMapping;
  3. import org.springframework.web.bind.annotation.PathVariable;
  4. import org.springframework.web.bind.annotation.PostMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import javax.annotation.Resource;
  7. @RestController
  8. public class DemoController {
  9.     @Resource
  10.     private IRemoteService iRemoteService;
  11.     @GetMapping("/getGreetingName")
  12.     public String getGreetingName() {
  13.         return iRemoteService.getGreetingName();
  14.     }
  15.     @PostMapping("/sayHello/{name}")
  16.     public String sayHello(@PathVariable("name") String name) {
  17.         return iRemoteService.sayHello(name);
  18.     }
  19. }
复制代码
  1. ###
  2. GET http://localhost:8080/getGreetingName
  3. HTTP/1.1 200
  4. Content-Type: text/plain;charset=UTF-8
  5. Content-Length: 16
  6. Date: Thu, 06 Oct 2022 12:28:45 GMT
  7. Connection: close
  8. getGreetingName:
  9. ###
  10. POST http://localhost:8080/sayHello/ketty
  11. HTTP/1.1 200
  12. Content-Type: text/plain;charset=UTF-8
  13. Content-Length: 14
  14. Date: Thu, 06 Oct 2022 12:30:40 GMT
  15. Connection: close
  16. sayHello:ketty
复制代码
通过测试我们可以看到,通过代理实现了远程接口的封装和调用,至此,一切正常,好像没毛病!!!可是,过了段时间就有同事找过来说依赖了我的sdk导致应用无法正常启动了。。。
问题分析:

通过报错的堆栈信息及debug跟踪,最后找到问题在Spring bean的创建过程中,registerDisposableBeanIfNecessary注册实现了Disposable Bean接口或者指定了destroy method的bean,亦或者是被指定的DestructionAwareBeanPostProcessor处理的bean,在bean销毁的时候执行对应的方法;我们看下如下代码片段:
  1. /**
  2. * Determine whether the given bean requires destruction on shutdown.
  3. * <p>The default implementation checks the DisposableBean interface as well as
  4. * a specified destroy method and registered DestructionAwareBeanPostProcessors.
  5. * @param bean the bean instance to check
  6. * @param mbd the corresponding bean definition
  7. * @see org.springframework.beans.factory.DisposableBean
  8. * @see AbstractBeanDefinition#getDestroyMethodName()
  9. * @see org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor
  10. */
  11. protected boolean requiresDestruction(Object bean, RootBeanDefinition mbd) {
  12.         return (bean.getClass() != NullBean.class && (DisposableBeanAdapter.hasDestroyMethod(bean, mbd) ||
  13.       // 判断是否有DestructionAwareBeanPostProcessors处理该bean
  14.                         (hasDestructionAwareBeanPostProcessors() && DisposableBeanAdapter.hasApplicableProcessors(
  15.                                         bean, getBeanPostProcessorCache().destructionAware))));
  16. }
复制代码
继续跟踪到 DisposableBeanAdapter.hasApplicableProcessors
  1. /**
  2. * Check whether the given bean has destruction-aware post-processors applying to it.
  3. * @param bean the bean instance
  4. * @param postProcessors the post-processor candidates
  5. */
  6. public static boolean hasApplicableProcessors(Object bean, List<DestructionAwareBeanPostProcessor> postProcessors) {
  7.         if (!CollectionUtils.isEmpty(postProcessors)) {
  8.                 for (DestructionAwareBeanPostProcessor processor : postProcessors) {
  9.       // 每个processor根据自己的具体情况实现requiresDestruction方法,默认是返回true
  10.                         if (processor.requiresDestruction(bean)) {
  11.                                 return true;
  12.                         }
  13.                 }
  14.         }
  15.         return false;
  16. }
复制代码
接下来,我们稍微改下代码来重现下该问题,加入spring-boot-starter-data-jpa 以及 mapper-spring-boot-starter依赖,重新启动应用之后,意想不到的事情发生了:
  1. // 应用启动报错了,这个异常正是我们代理处理类中定义的,
  2. // 说明应用启动的时候,调用了iRemoteService非声明的方法,这里打印出来的是【hashCode】方法
  3. Caused by: org.springframework.beans.factory.BeanCreationException:
  4. Error creating bean with name 'iRemoteService' defined in class path resource
  5. [com/example/remoteserviceproxydemo/RemoteServiceProxyDemoConfiguration.class]:
  6. Unexpected exception during bean creation; nested exception is java.lang.RuntimeException:
  7. method which is not declared, hashCode
复制代码
通过以上代码分析,我们找到了调用的地方,PersistenceAnnotationBeanPostProcessor.requiresDestruction` 方法,这里最终会执行注册bean的hashCode方法,由于是代理类,所以会执行InvocationHandler的invoke方法;而hashCode方法并不是我们IRemoteService接口类中声明的方法,所以会在checkMethod中抛出异常
  1. @Override
  2. public boolean requiresDestruction(Object bean) {
  3.   // 这里extendedEntityManagersToClose是ConcurrentHashMap
  4.         return this.extendedEntityManagersToClose.containsKey(bean);
  5. }
  6. // ConcurrentHashMap的containsKey方法
  7. /**
  8. * Tests if the specified object is a key in this table.
  9. *
  10. * @param  key possible key
  11. * @return {@code true} if and only if the specified object
  12. *         is a key in this table, as determined by the
  13. *         {@code equals} method; {@code false} otherwise
  14. * @throws NullPointerException if the specified key is null
  15. */
  16. public boolean containsKey(Object key) {
  17.     return get(key) != null;
  18. }
  19. /**
  20. * Returns the value to which the specified key is mapped,
  21. * or {@code null} if this map contains no mapping for the key.
  22. *
  23. * <p>More formally, if this map contains a mapping from a key
  24. * {@code k} to a value {@code v} such that {@code key.equals(k)},
  25. * then this method returns {@code v}; otherwise it returns
  26. * {@code null}.  (There can be at most one such mapping.)
  27. *
  28. * @throws NullPointerException if the specified key is null
  29. */
  30. public V get(Object key) {
  31.     Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
  32.     // 这里可以看到,调用了hashCode方法,由于该bean是代理类,
  33.     // 所以会执行RemoteServiceInvocationHandler的invoke方法,
  34.     // 从而抛出自定义异常throw new RuntimeException("method which is not declared, " + method.getName());
  35.     int h = spread(key.hashCode());
  36.     if ((tab = table) != null && (n = tab.length) > 0 &&
  37.         (e = tabAt(tab, (n - 1) & h)) != null) {
  38.         if ((eh = e.hash) == h) {
  39.             if ((ek = e.key) == key || (ek != null && key.equals(ek)))
  40.                 return e.val;
  41.         }
  42.         else if (eh < 0)
  43.             return (p = e.find(h, key)) != null ? p.val : null;
  44.         while ((e = e.next) != null) {
  45.             if (e.hash == h &&
  46.                 ((ek = e.key) == key || (ek != null && key.equals(ek))))
  47.                 return e.val;
  48.         }
  49.     }
  50.     return null;
  51. }
复制代码
解决方法:

总结:

虽说是个小问题也比较细节,但是,整个过程梳理下来还是涉及到很多的知识点:Spring boot启动过程;Spring bean的生命周期;Spring boot扩展BeanPostProcessor; FactoryBean的用法;动态注册Spring bean的几种方法;Java反射及代理等等。通过这些知识的梳理,重新回顾的同时也学到了一些新的知识,希望以后能多抓住这种排查问题和分析问题的机会,多多总结,少踩坑。
参考:

代码示例:


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4