背景介紹: 最近在搭建一個公共項目,類似業務操作記錄上報的功能,就想著給業務方提供統一的sdk,在sdk中實現客戶端和服務端的交互封裝,對業務方幾乎是無感的。訪問關係如下圖: 訪問關係示意圖 這裡採用了http的方式進行交互,但是,如果每次介面調用都需要感知http的封裝,一來代碼重覆度較高,二來新 ...
背景介紹:
最近在搭建一個公共項目,類似業務操作記錄上報的功能,就想著給業務方提供統一的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