Dubbo配置註冊中心設定application的name使用駝峰命名法存在的隱藏專案啟動異常問題

朱季謙發表於2021-12-21

原創/朱季謙

首先,先提一個建議,在SpringBoot+Dubbo專案中,Dubbo配置註冊中心設定的application命名name的值,最好使用xxx-xxx-xxx這樣格式的,避免隨便使用駝峰命名。因為使用駝峰命名法,在Spring的IOC容器當中,很可能會出現一些導致專案啟動失敗的坑,例如,會出現這樣的異常報錯:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'userService' is expected to be of type 'com.xxx.xxx.xxx.service.UserService' but was actually of type 'org.apache.dubbo.config.ApplicationConfig'

在說明該問題之前,首先,需要提一下org.apache.dubbo.config.ApplicationConfig這個類,這是一個Dubbo的應用配置類,它用在哪裡呢?

在SpringBoot 2.x+Dubbo專案當中,主流都是使用yaml檔案設定專案環境依賴引數,不同的元件,其配置類的例項化各有差異。本文主要簡單分析下Dubbo的相關配置類是如何通過yaml檔案引數來根據環境不同進行做相應的例項化。

Dubbo初始化配置類主要有以下——

序號 類名 用途 說明
1 ApplicationConfig 當前應用配置 提供者或者消費者配置當前應用資訊,一般以屬性name區分各應用
2 MonitorConfig 監控中心配置 配置連線監控中心monitor引數
3 RegistryConfig 連線註冊中心配置 配置Dubbo用到的註冊中心
4 ProtocolConfig 服務提供者協議配置 配置提供方的遠端通訊協議
...... ...... ...... ......

這些配置類的實現原理基本大同小異,本文主要以ApplicationConfig配置類做講解分析。

在yaml配置檔案裡,Dubbo的配置例子如下——

dubbo:
  application:
    name: userService
  registry:
    address: zookeeper://127.0.0.1:2181
    timeout: 10000
  protocol:
    name: dubbo
    port: 20880

這個配置可以拆開如下圖這樣看,便能一眼看懂它們分別屬於哪個配置類——
image

當在yaml檔案這樣配置後,當專案啟動時,會自動獲取這些引數,然後初始化到對應的配置類當中,例如,application中的name值就會設定到ApplicationConfig類物件裡——
image

在SpringBoot中,這個ApplicationConfig物件會在普通bean初始化之前,就已經裝載到IOC容器當中,以name的值做該bean名,同時,會以name:className的方式儲存在Spring的bean別名快取aliasMap當中,這就出現一個問題,假如該專案當中存在同名bean註解的話,會出現什麼樣情況呢?

例如,當SpringBoot的Dubbo配置如前邊一樣,以字串“userService”做ApplicationConfig的name值,同時,controller層有以下程式碼——

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;
  
    ......
}

我們可以在IOC容器過程的AbstractBeanFactory類中的doGetBean(String name, @Nullable Class requiredType, @Nullable Object[] args, boolean typeCheckOnly)方法截圖處打一個針對userService的斷點——

image

截圖裡的邏輯,其實是在對註解有@RestController的UserController類做IOC過程中,會對其通過 @Resource設定的屬性userService做依賴注入過程,首先,會去bean別名aliasMap快取當中看是否能查詢到,我們進入到transformedBeanName(name)方法底層——

public String canonicalName(String name) {
   String canonicalName = name;
   // Handle aliasing...
   String resolvedName;
   do {
      resolvedName = this.aliasMap.get(canonicalName);
      if (resolvedName != null) {
         canonicalName = resolvedName;
      }
   }
   while (resolvedName != null);
   return canonicalName;
}

此時,this.aliasMap快取裡已經有值了,主要都是Dubbo相關的,這說明Dubbo會在普通自定義Bean前就做了IOC注入,我們可以看到,前邊提到的ApplicationConfig物件class類名,已經快取在aliasMap當中,其key值,正好yaml配置檔案裡設定的name值。當以“userService”字串取aliasMap獲取,是可以拿到值的——

image

但是,這裡注意一點,此刻debug這一步doGetBean,理應依賴注入的是UserService類而不是ApplicationConfig類——

image

然而實際情況是,此時,通過方法Object sharedInstance = getSingleton(beanName)從IOC三級快取之一的單例池裡獲取到的則是ApplicationConfig已經初始化成單例bean的物件——

image

這將會出現什麼情況呢?

在 doGetBean方法最後,會做一步這樣操作,將需要初始化的bean型別requiredType與通過“userService”從單例池裡獲取到的實際bean型別做比較——

// Check if required type matches the type of the actual bean instance.
//檢查所需型別是否與實際bean例項的型別匹配
if (requiredType != null && !requiredType.isInstance(bean)) {
   try {
      T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
      if (convertedBean == null) {
         throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
      }
      return convertedBean;
   }
   catch (TypeMismatchException ex) {
      if (logger.isTraceEnabled()) {
         logger.trace("Failed to convert bean '" + name + "' to required type '" +
               ClassUtils.getQualifiedName(requiredType) + "'", ex);
      }
      throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
   }
}

結果可想而知,一個是UserService類,一個是ApplicationConfig類,兩者肯定不匹配,那麼就會執行丟擲異常throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());

public BeanNotOfRequiredTypeException(String beanName, Class<?> requiredType, Class<?> actualType) {
   super("Bean named '" + beanName + "' is expected to be of type '" + ClassUtils.getQualifiedName(requiredType) +
         "' but was actually of type '" + ClassUtils.getQualifiedName(actualType) + "'");
   this.beanName = beanName;
   this.requiredType = requiredType;
   this.actualType = actualType;
}

debug到這一步,其錯誤提示,剛好就是——

Bean named 'userService' is expected to be of type 'com.xxx.xxx.xxx.service.UserService' but was actually of type 'org.apache.dubbo.config.ApplicationConfig

因此,就說明一個問題,當Dubbo應用配置application的name使用駝峰命名,例如,本文中的userService,剛好又有某個地方用到類似這樣註解的屬性依賴注入 private UserService userService,那麼,專案在啟動過程中,就會出現類似本文中提到的專案啟動異常。

可見,在application的name值使用xxx-xxx-xx這樣方式命名會更好些。

相關文章