背景介紹:
最近在搭建一個公共專案,類似業務操作記錄上報的功能,就想著給業務方提供統一的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注冊iRemoteServicepackage 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
代碼示例:
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/514995.html
標籤:Java
上一篇:day48-JDBC和連接池04
下一篇:python基礎-字串常用方法
