动态代理类注册为Spring Bean的坑(动态代理 注解)

  本篇文章为你整理了动态代理类注册为Spring Bean的坑(动态代理 注解)的详细内容,包含有spring的动态代理有几种实现方式 动态代理 注解 spring中的动态代理是哪一种 spring动态代理的两种方式 动态代理类注册为Spring Bean的坑,希望能帮助你了解 动态代理类注册为Spring Bean的坑。

  背景介绍:

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

  访问关系示意图

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

  写个简单的例子来验证一下:

  假设有个远程服务,提供了如下接口:

  

package com.example.remoteserviceproxydemo;

 

   * IRemoteService

   * @author beetle_shu

  public interface IRemoteService {

   * getGreetingName

   * @return

   String getGreetingName();

   * sayHello

   * @param name

   * @return

   String sayHello(String name);

  

 

  接下来,我们自定义一个InvocationHandler 来实现远程方法的调用

  

package com.example.remoteserviceproxydemo;

 

  import java.lang.reflect.InvocationHandler;

  import java.lang.reflect.Method;

   * RemoteServiceInvocationHandler

   * @author beetle_shu

  public class RemoteServiceInvocationHandler implements InvocationHandler {

   @Override

   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   // 如果是远程http服务调用,通常有以下几步:

   // 1. 解析方法和参数:可以通过自定义注解,在方法上定义远程服务地址,请求方式GET/POST等信息

   // 2. 采用httpclient,OkHttp,或者restTemplate进行远程服务调用

   // 3. 解析http响应,反序列化成对应接口方法的返回对象

   // 这里,我们就不真正调用服务了,伪代码只是验证下被调用的方法是不是我们自己定义的,

   // 如果是的话返回当前方法名,如果不是的话,抛出异常,程序中断

   checkMethod(method);

   String methodName = method.getName();

   String param = "";

   if (args != null args.length 0) {

   param = String.valueOf(args[0]);

   return methodName + ":" + param;

   private void checkMethod(Method method) {

   Method[] methods = IRemoteService.class.getDeclaredMethods();

   for (Method m : methods) {

   if (m.getName().equals(method.getName())) {

   return;

   throw new RuntimeException("method which is not declared, " + method.getName());

  

 

  紧接着,通过java.lang.reflect.Proxy代理类创建一个代理对象,代理远程服务的调用,同时把该对象注册为Spring bean,加入Spring容器

  

package com.example.remoteserviceproxydemo;

 

  import org.springframework.context.annotation.Bean;

  import org.springframework.context.annotation.Configuration;

  import java.lang.reflect.Proxy;

  @Configuration

  public class RemoteServiceProxyDemoConfiguration {

   @Bean

   public IRemoteService getRemoteService() {

   return (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),

   new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());

  

 

  最后,我们创建一个Controller来调用测试一下:

  

package com.example.remoteserviceproxydemo;

 

  import org.springframework.web.bind.annotation.GetMapping;

  import org.springframework.web.bind.annotation.PathVariable;

  import org.springframework.web.bind.annotation.PostMapping;

  import org.springframework.web.bind.annotation.RestController;

  import javax.annotation.Resource;

  @RestController

  public class DemoController {

   @Resource

   private IRemoteService iRemoteService;

   @GetMapping("/getGreetingName")

   public String getGreetingName() {

   return iRemoteService.getGreetingName();

   @PostMapping("/sayHello/{name}")

   public String sayHello(@PathVariable("name") String name) {

   return iRemoteService.sayHello(name);

  

 

  

###

 

  GET http://localhost:8080/getGreetingName

  HTTP/1.1 200

  Content-Type: text/plain;charset=UTF-8

  Content-Length: 16

  Date: Thu, 06 Oct 2022 12:28:45 GMT

  Connection: close

  getGreetingName:

  POST http://localhost:8080/sayHello/ketty

  HTTP/1.1 200

  Content-Type: text/plain;charset=UTF-8

  Content-Length: 14

  Date: Thu, 06 Oct 2022 12:30:40 GMT

  Connection: close

  sayHello:ketty

  

 

  通过测试我们可以看到,通过代理实现了远程接口的封装和调用,至此,一切正常,好像没毛病!!!可是,过了段时间就有同事找过来说依赖了我的sdk导致应用无法正常启动了。。。

  问题分析:

  通过报错的堆栈信息及debug跟踪,最后找到问题在Spring bean的创建过程中,registerDisposableBeanIfNecessary注册实现了Disposable Bean接口或者指定了destroy method的bean,亦或者是被指定的DestructionAwareBeanPostProcessor处理的bean,在bean销毁的时候执行对应的方法;我们看下如下代码片段:

  

/**

 

   * Determine whether the given bean requires destruction on shutdown.

   * p The default implementation checks the DisposableBean interface as well as

   * a specified destroy method and registered DestructionAwareBeanPostProcessors.

   * @param bean the bean instance to check

   * @param mbd the corresponding bean definition

   * @see org.springframework.beans.factory.DisposableBean

   * @see AbstractBeanDefinition#getDestroyMethodName()

   * @see org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor

  protected boolean requiresDestruction(Object bean, RootBeanDefinition mbd) {

   return (bean.getClass() != NullBean.class (DisposableBeanAdapter.hasDestroyMethod(bean, mbd)

   // 判断是否有DestructionAwareBeanPostProcessors处理该bean

   (hasDestructionAwareBeanPostProcessors() DisposableBeanAdapter.hasApplicableProcessors(

   bean, getBeanPostProcessorCache().destructionAware))));

  

 

  继续跟踪到 DisposableBeanAdapter.hasApplicableProcessors

  

/**

 

   * Check whether the given bean has destruction-aware post-processors applying to it.

   * @param bean the bean instance

   * @param postProcessors the post-processor candidates

  public static boolean hasApplicableProcessors(Object bean, List DestructionAwareBeanPostProcessor postProcessors) {

   if (!CollectionUtils.isEmpty(postProcessors)) {

   for (DestructionAwareBeanPostProcessor processor : postProcessors) {

   // 每个processor根据自己的具体情况实现requiresDestruction方法,默认是返回true

   if (processor.requiresDestruction(bean)) {

   return true;

   return false;

  

 

  接下来,我们稍微改下代码来重现下该问题,加入spring-boot-starter-data-jpa 以及 mapper-spring-boot-starter依赖,重新启动应用之后,意想不到的事情发生了:

  

// 应用启动报错了,这个异常正是我们代理处理类中定义的,

 

  // 说明应用启动的时候,调用了iRemoteService非声明的方法,这里打印出来的是【hashCode】方法

  Caused by: org.springframework.beans.factory.BeanCreationException:

  Error creating bean with name iRemoteService defined in class path resource

  [com/example/remoteserviceproxydemo/RemoteServiceProxyDemoConfiguration.class]:

  Unexpected exception during bean creation; nested exception is java.lang.RuntimeException:

  method which is not declared, hashCode

  

 

  通过以上代码分析,我们找到了调用的地方,PersistenceAnnotationBeanPostProcessor.requiresDestruction` 方法,这里最终会执行注册bean的hashCode方法,由于是代理类,所以会执行InvocationHandler的invoke方法;而hashCode方法并不是我们IRemoteService接口类中声明的方法,所以会在checkMethod中抛出异常

  

@Override

 

  public boolean requiresDestruction(Object bean) {

   // 这里extendedEntityManagersToClose是ConcurrentHashMap

   return this.extendedEntityManagersToClose.containsKey(bean);

  // ConcurrentHashMap的containsKey方法

   * Tests if the specified object is a key in this table.

   * @param key possible key

   * @return {@code true} if and only if the specified object

   * is a key in this table, as determined by the

   * {@code equals} method; {@code false} otherwise

   * @throws NullPointerException if the specified key is null

  public boolean containsKey(Object key) {

   return get(key) != null;

   * Returns the value to which the specified key is mapped,

   * or {@code null} if this map contains no mapping for the key.

   * p More formally, if this map contains a mapping from a key

   * {@code k} to a value {@code v} such that {@code key.equals(k)},

   * then this method returns {@code v}; otherwise it returns

   * {@code null}. (There can be at most one such mapping.)

   * @throws NullPointerException if the specified key is null

  public V get(Object key) {

   Node K,V [] tab; Node K,V e, p; int n, eh; K ek;

   // 这里可以看到,调用了hashCode方法,由于该bean是代理类,

   // 所以会执行RemoteServiceInvocationHandler的invoke方法,

   // 从而抛出自定义异常throw new RuntimeException("method which is not declared, " + method.getName());

   int h = spread(key.hashCode());

   if ((tab = table) != null (n = tab.length) 0

   (e = tabAt(tab, (n - 1) h)) != null) {

   if ((eh = e.hash) == h) {

   if ((ek = e.key) == key (ek != null key.equals(ek)))

   return e.val;

   else if (eh 0)

   return (p = e.find(h, key)) != null ? p.val : null;

   while ((e = e.next) != null) {

   if (e.hash == h

   ((ek = e.key) == key (ek != null key.equals(ek))))

   return e.val;

   return null;

  

 

  解决方法:

  
不用代理类,写个具体实现类

  这种方法跟我们初衷有点相背离,以后接口新增修改也都要改sdk中的实现类,具体实现如下:

  

package com.example.remoteserviceproxydemo;

 

  import java.lang.reflect.Proxy;

  // 定义具体的实现类

  public class RemoteServiceImpl implements IRemoteService {

   private IRemoteService iRemoteService;

   public RemoteServiceImpl() {

   this.iRemoteService = (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),

   new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());

   @Override

   public String getGreetingName() {

   return iRemoteService.getGreetingName();

   @Override

   public String sayHello(String name) {

   return iRemoteService.sayHello(name);

  

 

  

package com.example.remoteserviceproxydemo;

 

  import org.springframework.context.annotation.Bean;

  import org.springframework.context.annotation.Configuration;

  import java.lang.reflect.Proxy;

  @Configuration

  public class RemoteServiceProxyDemoConfiguration {

   @Bean("iRemoteService")

   public IRemoteService getRemoteService() {

  // 注册的bean也改为具体实现类,这样就可以绕过代理类没有【hashCode】方法的问题了

   return new RemoteServiceImpl();

  // return (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),

  // new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());

  

 

  
用代理类,在invoke方法中对【hashCode】方法调用做特殊处理

  这种方法也是参考feign的实现,改起来也比较简单,invoke方法进来先判断是hashCode/equals/toString方法,就执行重写的hashCode/equals/toString方法,改写RemoteServiceInvocationHandler如下 :

  

package com.example.remoteserviceproxydemo;

 

  import java.lang.reflect.InvocationHandler;

  import java.lang.reflect.Method;

  import java.lang.reflect.Proxy;

   * RemoteServiceInvocationHandler

   * @author beetle_shu

  public class RemoteServiceInvocationHandler implements InvocationHandler {

   @Override

   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   // 新增对hashCode/equals/toString方法的处理

   if ("equals".equals(method.getName())) {

   try {

   Object otherHandler =

   args.length 0 args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;

   // 可以根据实际情况重写【equals】方法

   return this.equals(otherHandler);

   } catch (IllegalArgumentException e) {

   return false;

   } else if ("hashCode".equals(method.getName())) {

   // 可以根据实际情况重写【hashCode】方法

   return this.hashCode();

   } else if ("toString".equals(method.getName())) {

   // 可以根据实际情况重写【toString】方法

   return this.toString();

   // 如果是远程http服务调用,通常有以下几步:

   // 1. 解析方法和参数:可以通过自定义注解,在方法上定义远程服务地址,请求方式GET/POST等信息

   // 2. 采用httpclient,OkHttp,或者restTemplate进行远程服务调用

   // 3. 解析http响应,反序列化成对应接口方法的返回对象

   // 这里,我们就不真正调用服务了,伪代码仅返回当前方法名

   checkMethod(method);

   String methodName = method.getName();

   String param = "";

   if (args != null args.length 0) {

   param = String.valueOf(args[0]);

   return methodName + ":" + param;

   private void checkMethod(Method method) {

   Method[] methods = IRemoteService.class.getDeclaredMethods();

   for (Method m : methods) {

   if (m.getName().equals(method.getName())) {

   return;

   throw new RuntimeException("method which is not declared, " + method.getName());

  

 

  
用FactoryBean的getObject返回代理类,并且自定义BeanDefinitionRegistrar注册bean

  这种方法也是我比较推荐的,很好的利用了Spring的扩展,进行动态bean的注册;当然,结合第2种方法一起实现,应该会完美:

  

package com.example.remoteserviceproxydemo;

 

  import org.springframework.beans.factory.FactoryBean;

  import java.lang.reflect.Proxy;

   * 定义RemoteServiceFactoryBean

   * @author beetle_shu

  public class RemoteServiceFactoryBean implements FactoryBean IRemoteService {

   @Override

   public IRemoteService getObject() throws Exception {

   return (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),

   new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());

   @Override

   public Class ? getObjectType() {

   return IRemoteService.class;

   @Override

   public boolean isSingleton() {

   return true;

  

 

  自定义BeanDefinitionRegistryPostProcessor 并且通过FactoryBean注册iRemoteService

  

package com.example.remoteserviceproxydemo;

 

  import org.springframework.beans.BeansException;

  import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

  import org.springframework.beans.factory.support.BeanDefinitionBuilder;

  import org.springframework.beans.factory.support.BeanDefinitionRegistry;

  import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;

   * RemoteServiceBeanDefinitionRegistryPostProcessor

   * @author beetle_shu

  public class RemoteServiceBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {

   @Override

   public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

   BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(RemoteServiceFactoryBean.class);

   registry.registerBeanDefinition("iRemoteService", definitionBuilder.getBeanDefinition());

   @Override

   public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

  

 

  修改下配置类,通过@Import加载RemoteServiceBeanDefinitionRegistryPostProcessor

  

package com.example.remoteserviceproxydemo;

 

  import org.springframework.context.annotation.Bean;

  import org.springframework.context.annotation.Configuration;

  import org.springframework.context.annotation.Import;

  import java.lang.reflect.Proxy;

  @Configuration

  @Import(RemoteServiceBeanDefinitionRegistryPostProcessor.class)

  public class RemoteServiceProxyDemoConfiguration {

  // @Bean("iRemoteService")

  // public IRemoteService getRemoteService() {

  //// return new RemoteServiceImpl();

  // return (IRemoteService) Proxy.newProxyInstance(IRemoteService.class.getClassLoader(),

  // new Class[] { IRemoteService.class }, new RemoteServiceInvocationHandler());

  

 

  
重写PersistenceAnnotationBeanPostProcessor

  个人不太建议用这种方式,除非对Spring框架有比较透彻的理解以及对源代码有比较高的把控度,具体实现可以参考该大神的文章:https://www.huluohu.com/posts/202102252023/

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

  如何记忆 Spring Bean 的生命周期 https://juejin.cn/post/6844904065457979405

  三万字盘点Spring/Boot的那些扩展点 https://mdnice.com/writing/97dd3ca064304bc9b8d3231dbba2f3b8

  jpa调用远程代理类的hashcode方法导致无法初始化的问题 https://www.huluohu.com/posts/202102252023/

  动态注册bean,Spring官方套路:使用BeanDefinitionRegistryPostProcessor https://zhuanlan.zhihu.com/p/30590254

  使用BeanDefinitionRegistryPostProcessor动态注入BeanDefinition https://www.jianshu.com/p/b4bec64ada70

  代码示例:

  以上就是动态代理类注册为Spring Bean的坑(动态代理 注解)的详细内容,想要了解更多 动态代理类注册为Spring Bean的坑的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: