jjzjj

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

hateButLoveIt 2023-04-16 原文

背景介绍:

最近在搭建一个公共项目,类似业务操作记录上报的功能,就想着给业务方提供统一的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;
}

解决方法:

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

    这种方法跟我们初衷有点相背离,以后接口新增修改也都要改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());
        }
    
    }
    
  2. 用代理类,在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());
        }
    }
    
  3. 用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());
    //    }
    
    }
    
  4. 重写PersistenceAnnotationBeanPostProcessor

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

总结:

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

参考:

  1. 如何记忆 Spring Bean 的生命周期 https://juejin.cn/post/6844904065457979405
  2. 三万字盘点Spring/Boot的那些扩展点 https://mdnice.com/writing/97dd3ca064304bc9b8d3231dbba2f3b8
  3. jpa调用远程代理类的hashcode方法导致无法初始化的问题 https://www.huluohu.com/posts/202102252023/
  4. 动态注册bean,Spring官方套路:使用BeanDefinitionRegistryPostProcessor https://zhuanlan.zhihu.com/p/30590254
  5. 使用BeanDefinitionRegistryPostProcessor动态注入BeanDefinition https://www.jianshu.com/p/b4bec64ada70

代码示例:

有关动态代理类注册为Spring Bean的坑的更多相关文章

  1. 阿里云国际版免费试用:如何注册以及注意事项 - 2

    作为新的阿里云用户,您可以50免费试用多种优惠,价值高达1,700美元(或8,500美元)。这将让您了解和体验阿里云平台上提供的一系列产品和服务。如果您以个人身份注册免费试用,您将获得价值1,700美元的优惠。但是,如果您是注册公司,您可以选择企业免费试用,提交基本信息通过企业实名注册验证,即可开始价值$8,500的免费试用!本教程介绍了如何设置您的帐户并使用您的免费试用版。​关于免费试用在我们开始此试用之前,您还必须遵守以下条款和条件才能访问您的免费试用:只有在一年内创建的账户才有资格获得阿里云免费试用。通过此免费试用优惠,用户可以免费试用免费试用活动页面上列出的每种产品一次。如果您有多个帐

  2. ruby - 在 Ruby 中动态创建数组 - 2

    有没有办法在Ruby中动态创建数组?例如,假设我想遍历用户输入的书籍数组:books=gets.chomp用户输入:"TheGreatGatsby,CrimeandPunishment,Dracula,Fahrenheit451,PrideandPrejudice,SenseandSensibility,Slaughterhouse-Five,TheAdventuresofHuckleberryFinn"我把它变成一个数组:books_array=books.split(",")现在,对于用户输入的每一本书,我想用Ruby创建一个数组。伪代码来做到这一点:x=0books_array.

  3. ruby-on-rails - 设计注册确认 - 2

    我在我的项目中有一个用户和一个管理员角色。我使用Devise创建了身份验证。在我的管理员角色中,我没有任何确认。在我的用户模型中,我有以下内容:devise:database_authenticatable,:confirmable,:recoverable,:rememberable,:trackable,:validatable,:timeoutable,:registerable#Setupaccessible(orprotected)attributesforyourmodelattr_accessible:email,:username,:prename,:surname,:

  4. ruby - 是否可以将 IRB 提示配置为动态更改? - 2

    我想在IRB中浏览文件系统并让提示更改以反射(reflect)当前工作目录,但我不知道如何在每个命令后进行提示更新。最终,我想在日常工作中更多地使用IRB,让bash溜走。我在我的.irbrc中试过这个:require'fileutils'includeFileUtilsIRB.conf[:PROMPT][:CUSTOM]={:PROMPT_N=>"\e[1m:\e[m",:PROMPT_I=>"\e[1m#{pwd}>\e[m",:PROMPT_S=>"FOO",:PROMPT_C=>"\e[1m#{pwd}>\e[m",:RETURN=>""}IRB.conf[:PROMPT_MO

  5. ruby - HTTP 请求中的用户代理,Ruby - 2

    我是Ruby的新手。我试过查看在线文档,但没有找到任何有效的方法。我想在以下HTTP请求botget_response()和get()中包含一个用户代理。有人可以指出我正确的方向吗?#PreliminarycheckthatProggitisupcheck=Net::HTTP.get_response(URI.parse(proggit_url))ifcheck.code!="200"puts"ErrorcontactingProggit"returnend#Attempttogetthejsonresponse=Net::HTTP.get(URI.parse(proggit_url)

  6. ruby-on-rails - capybara poltergeist - 覆盖用户代理 - 2

    有人知道如何将capybarapoltergeist的用户代理覆盖到移动用户代理以进行测试吗?我发现了一些有关为seleniumwebdriver配置它的信息:http://blog.plataformatec.com.br/2011/03/configuring-user-agents-with-capybara-selenium-webdriver/这在capybara闹鬼中怎么可能? 最佳答案 请参阅poltergeistgithub页面上的链接:https://github.com/teampoltergeist/polte

  7. ruby - 如何配置 Ruby Mechanize 代理以通过 Charles Web 代理工作? - 2

    我正在使用Ruby/Mechanize编写一个“自动填写表格”应用程序。它几乎可以工作。我可以使用精彩CharlesWeb代理以查看服务器和我的Firefox浏览器之间的交换。现在我想使用Charles查看服务器和我的应用程序之间的交换。Charles在端口8888上代理。假设服务器位于https://my.host.com。.一件不起作用的事情是:@agent||=Mechanize.newdo|agent|agent.set_proxy("my.host.com",8888)end这会导致Net::HTTP::Persistent::Error:...lib/net/http/pe

  8. ruby-on-rails - carrierwave:在序列化动态属性上安装 uploader - 2

    首先,我使用的是rails3.1.3和来自master的carrierwavegithub仓库的分支。我使用after_init钩子(Hook)来确定基于属性的字段页面模型实例并为这些字段定义属性访问器将值存储在序列化哈希中(希望它清楚我是什么谈论)。这是我正在做的事情的精简版:classPage省略mount_uploader命令让我可以访问我想要的属性。但是当我安装uploader时出现错误消息说“nil类的未定义新方法”我在源代码中读到有方法read_uploader和扩展模块中的write_uploader。我如何必须覆盖这些来制作mount_uploader命令使用我的“虚拟

  9. ruby - 如何捕获所有 HTTP 流量(本地代理) - 2

    我希望访问我机器上的所有HTTP流量(我的Windows机器-不是服务器)。据我了解,拥有一个本地代理是所有流量路线的必经之路。我一直在谷歌搜索但未能找到任何资源(关于Ruby)来帮助我。非常感谢任何提示或链接。 最佳答案 WEBrick中有一个HTTP代理(Rubystdlib的一部分)和here's一个实现示例。如果你喜欢生活在边缘,还有em-proxy伊利亚·格里戈里克。这postIlya暗示它似乎确实需要一些调整来解决您的问题。 关于ruby-如何捕获所有HTTP流量(本地代理)

  10. ruby - 在 Ruby 中动态生成多维数组 - 2

    我正在尝试动态构建一个多维数组。我想要的基本上是这样的(为简单起见写出来):b=0test=[[]]test[b]这给了我错误:NoMethodError:undefinedmethod`test=[[],[],[]]而且它工作正常,但在我的实际使用中,我不会事先知道需要多少个数组。有一个更好的方法吗?谢谢 最佳答案 不需要像您正在使用的索引变量。只需将每个数组附加到您的test数组:irb>test=[]=>[]irb>test[["a","b","c"]]irb>test[["a","b","c"],["d","e","f"]]

随机推荐