<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      深入認識ClassLoader - 一次投產(chǎn)失敗的復盤

      問題背景

      投產(chǎn)日,同事負責的項目新版本發(fā)布,版本包是SpringBoot v2.7.18的一個FatJarjava -jar啟動報錯停止了,輸出的異常日志如下:

      Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [com/alibaba/druid/spring/boot/autoconfigure/DruidDataSourceAutoConfigure.class]: Invocation of init method failed; nested exception is org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class
      
              ...省略
              
      Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class
              at org.springframework.boot.autoconfigure.jdbc.DataSourceProperties.determineDriverClassName(DataSourceProperties.java:186)
              at org.springframework.boot.autoconfigure.jdbc.DataSourceProperties.determineUsername(DataSourceProperties.java:280)
              at com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceWrapper.afterPropertiesSet(DruidDataSourceWrapper.java:40)
              
             ...省略
      

      版本回退,正好我也在旁邊,記錄下一起排查解決的過程。

      定位與解決問題

      分析錯誤日志

      拉了版本分支代碼,從下往上看輸出的錯誤日志,發(fā)現(xiàn)是DruidDataSourceWrapper這個類中40行出錯,看下這個類以及出錯的位置:

      @ConfigurationProperties("spring.datasource.druid")
      class DruidDataSourceWrapper extends DruidDataSource implements InitializingBean {
          @Autowired
          private DataSourceProperties basicProperties;
      
          @Override
          public void afterPropertiesSet() throws Exception {
              //if not found prefix 'spring.datasource.druid' jdbc properties ,'spring.datasource' prefix jdbc properties will be used.
              if (super.getUsername() == null) {
                  // 關鍵行:這一行出錯,basicProperties.determineUsername()這個方法會出現(xiàn)異常
                  super.setUsername(basicProperties.determineUsername());
              }
              if (super.getPassword() == null) {
                  super.setPassword(basicProperties.determinePassword());
              }
              if (super.getUrl() == null) {
                  super.setUrl(basicProperties.determineUrl());
              }
              if (super.getDriverClassName() == null) {
                  super.setDriverClassName(basicProperties.getDriverClassName());
              }
          }
          ...
      

      DruidDataSourceWrapper歸屬于druid-spring-boot-starter這個依賴,是 alibaba druid 數(shù)據(jù)庫連接池的一個 starter。

      結合錯誤日志看下basicProperties.determineUsername()這個方法里面出錯的位置:

      public String determineUsername() {
          if (StringUtils.hasText(this.username)) {
              return this.username;
          }
          // 關鍵行:調用determineDriverClassName()這個方法出錯
          if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) {
              return "sa";
          }
          return null;
      }
      

      再次結合錯誤日志看下determineDriverClassName()這個方法里面出錯的位置:

      public String determineDriverClassName() {
          if (StringUtils.hasText(this.driverClassName)) {
              Assert.state(driverClassIsLoadable(), () -> "Cannot load driver class: " + this.driverClassName);
              return this.driverClassName;
          }
          String driverClassName = null;
          if (StringUtils.hasText(this.url)) {
              driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
          }
          if (!StringUtils.hasText(driverClassName)) {
              driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
          }
          if (!StringUtils.hasText(driverClassName)) {
              // 關鍵行:在這邊拋出的異常
              throw new DataSourceBeanCreationException("Failed to determine a suitable driver class", this,
                      this.embeddedDatabaseConnection);
          }
          return driverClassName;
      }
      

      定位到了出錯的位置,分析這塊代碼拋出異常的原因,意思就是如果spring.datasource.druid.username這個配置的值為空,那么讀取spring.datasource.username這個配置,如果還是空,嘗試從spring.datasource.url配置信息中解析jdbc驅動類,解析不出來就拋出DataSourceBeanCreationException異常。

      版本變動點

      是配置信息有問題?

      問了下這個項目的配置原本是放在配置文件中的,公共配置放在了application.yml中,不同環(huán)境的配置采用application-{profile}.yml放置,如下:

      application.yml
      application-dev.yml
      ...
      application-pro.yml
      

      application.yml中使用占位符借助 maven 打包時添加-P參數(shù)設置激活的profile

      spring:
        profiles:
          # env
          active: @env@
      

      項目 pom 文件中多個 profile 配置如下(這是本次版本的一個變動點):

      <profiles>
          <!-- DEV 開發(fā)環(huán)境-->
          <profile>
              <id>dev</id>
              <properties>
                  <env>DEV</env>
                  ...
              </properties>
          </profile>
          ...
          <!-- PRO 生產(chǎn)環(huán)境-->
          <profile>
              <id>pro</id>
              <properties>
                  <env>PRO</env>
                  ...
              </properties>
          </profile>
      </profiles>
      
      

      maven 打生產(chǎn)包,spring.profiles.active的值被設置成了PRO,也就是生產(chǎn)環(huán)境將使用application-PRO.yml這個配置文件。

      這個版本的另一個變動點是接入了 apollo 配置中心,但是沒有刪除不同環(huán)境的配置文件,配置文件application.yml中增加了 apollo 相關的配置:

      app:
        id: app-xxx-web
      apollo:
        bootstrap:
          namespaces: application
          enabled: true
        eagerLoad:
          enabled: true
      
      分析 SpringBoot 的配置加載流程
      觸發(fā)時機

      SpringBoot 應用啟動時在 SpringApplication prepareEnvironment方法中發(fā)布ApplicationEnvironmentPreparedEvent事件,EnvironmentPostProcessorApplicationListener 中監(jiān)聽了這個事件觸發(fā)配置信息讀取,不同來源的配置信息有專門實現(xiàn)了EnvironmentPostProcessor接口的類進行處理,這些類實現(xiàn)postProcessEnvironment方法,apollo-client使用的是v1.9.0版本,其包含一個META-INF/spring.factories

      org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
      com.ctrip.framework.apollo.spring.boot.ApolloAutoConfiguration
      org.springframework.context.ApplicationContextInitializer=\
      com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer
      org.springframework.boot.env.EnvironmentPostProcessor=\
      com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer
      

      com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer 會被掃描到,然后執(zhí)行其postProcessEnvironment方法,多個EnvironmentPostProcessor的執(zhí)行順序由其內部的order屬性決定,越小的越靠前,ApolloApplicationContextInitializerorder為0,屬于是靠后的:

      SpringBoot 中,后加載的屬性源可以覆蓋先加載的屬性源定義的值,參考:屬性源的優(yōu)先級順序,因此 apollo 中的配置會覆蓋配置文件中的配置。

      難道是 apollo 中的配置寫錯了?

      看了下 apollo 中沒有spring.datasource.url這個配置,數(shù)據(jù)庫的連接信息是寫在spring.datasource.druid這個前綴開頭下面的,apollo 中有兩個名為application的命名空間,一個格式是properties,另一個格式是yml,這些配置是寫在yml格式命名空間下的,properties格式命名空間下的配置為空。

      spring:
        # druid pool
        datasource:
          druid:
            url: jdbc:mysql://...:3306/...?useUnicode=true&characterEncoding=UTF-8&useSSL=false&...
            username: ...
            password: ...
            driver-class-name: com.mysql.cj.jdbc.Driver
          type: com.alibaba.druid.pool.DruidDataSource
          ...
      

      idea 啟動參數(shù)指定 apollo 配置,啟動項目,本地 apollo 的緩存文件夾config-cache下是有配置文件存在的,不過只有一個文件app-xx-web+default+application.properties,里面是空的。

      yml格式命名空間下的配置呢?

      看了下 apollo 的文檔,原來yml格式命名空間下的配置在客戶端使用需要填寫帶后綴的完整名字

      注1:yaml/yml格式的namespace從1.3.0版本開始支持和Spring整合,注入時需要填寫帶后綴的完整名字,比如application.yml

      注2:非properties、非yaml/yml格式(如xml,json等)的namespace暫不支持和Spring整合。

      配置文件application.yml中修改apollo的配置,將namespacesapplication修改為application.yml

      app:
        id: app-xxx-web
      apollo:
        bootstrap:
          namespaces: application.yml
          enabled: true
        eagerLoad:
          enabled: true
      

      本地調試啟動ok,apollo 中的配置可以正常拉取,項目啟動成功。

      生產(chǎn)環(huán)境 apollo 中的配置沒有生效的話,可application-{profile}.yml文件還在,應該還是能讀取配置文件中的配置完成啟動的吧?

      額,不對, maven 打生產(chǎn)包,spring.profiles.active的值被設置成了PRO,但classpath下生產(chǎn)環(huán)境配置文件名稱為 application-pro.yml,大小寫不一致,能正常加載嗎?

      application.yml配置文件中的app.apollo.bootstrap.namespaces配置還原,在 maven 的 Profiles 中勾選 dev ,spring.profiles.active的值被設置成了DEV,idea 中正常啟動項目,說明 application-dev.yml這個配置文件被讀取了。

      拿生產(chǎn)包在本地java -jar啟動,apollo 的配置服務器指定為dev環(huán)境,和生產(chǎn)環(huán)境報一樣的錯誤:

      java -Dapp.id=app-xxx-web -Dapollo.meta=http://10.100.x.x:8072 -jar app-xxx-web.jar
      

      難道是 CICD 打包的問題?

      沒有加載的配置文件

      本地打了一個包,啟動也是報一樣的錯誤,奇怪了,idea 里面啟動和打成 FatJar 之后啟動的行為還不一樣。

      idea 里面啟動,spring.profiles.active 的值是大寫的 DEVapplication-dev.yml中的配置是能正常讀取的,打成FatJar之后,spring.profiles.active的值是大寫的 PROapplication-pro.yml中的配置卻不能正常讀取。

      apollo 的 app.id 這個配置是放在application.yml中的,啟動后本地 apollo 的配置緩存文件夾 config-cache 下是有配置的,說明 application.yml 是生效的,只是不同環(huán)境 application-{profile}.yml 文件中的配置沒有生效。

      得著重看看 SpringBoot 中讀取配置文件的邏輯了。

      配置文件的加載流程

      上面分析到,EnvironmentPostProcessorApplicationListener 中監(jiān)聽了ApplicationEnvironmentPreparedEvent事件做配置信息讀取動作,不同來源的配置信息有專門實現(xiàn)了EnvironmentPostProcessor接口的類進行處理,配置文件的處理類是哪一個?

      debug 看了下,是 ConfigDataEnvironmentPostProcessor,其 postProcessEnvironment 方法里面進行處理,然后調用了ConfigDataEnvironment類中的 processAndApply 方法,其內部會調用processWithProfiles方法:

      private ConfigDataEnvironmentContributors processWithProfiles(ConfigDataEnvironmentContributors contributors,
              ConfigDataImporter importer, ConfigDataActivationContext activationContext) {
          this.logger.trace("Processing config data environment contributors with profile activation context");
          // 在這~~~
          contributors = contributors.withProcessedImports(importer, activationContext);
          registerBootstrapBinder(contributors, activationContext, ALLOW_INACTIVE_BINDING);
          return contributors;
      }
      

      此時的contributorsConfigDataEnvironmentContributors,繼續(xù)跟蹤 withProcessedImports 方法,里面會調用是ConfigDataImporterresolveAndLoad 方法:

      /**
       * Resolve and load the given list of locations, filtering any that have been
       * previously loaded.
       * @param activationContext the activation context
       * @param locationResolverContext the location resolver context
       * @param loaderContext the loader context
       * @param locations the locations to resolve
       * @return a map of the loaded locations and data
       */
      Map<ConfigDataResolutionResult, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext,
              ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext,
              List<ConfigDataLocation> locations) {
          try {
              // 關鍵行:定位出使用的環(huán)境profile
              Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;
              // 關鍵行:根據(jù)profile列出需要查找的配置文件列表
              List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);
              return load(loaderContext, resolved);
          }
          catch (IOException ex) {
              throw new IllegalStateException("IO error on loading imports from " + locations, ex);
          }
      }
      

      因為我本地 debug 的時候 profile 指定的 dev,所以spring.profiles.active的值被設置成了DEV

      繼續(xù)斷點,跟蹤到了StandardConfigDataLocationResolver類,其 getProfileSpecificReferences 方法中根據(jù) profile 列出需要讀取的配置文件路徑列表:

      繼續(xù)斷點到了獲取配置文件資源的位置,是 resolveNonPattern 方法:

      private List<StandardConfigDataResource> resolveNonPattern(StandardConfigDataReference reference) {
          // 關鍵行:通過統(tǒng)一的 ResourceLoader 接口獲取資源
          Resource resource = this.resourceLoader.getResource(reference.getResourceLocation());
          // 關鍵行:調用 Resource.exists() 方法,如果文件存在,則繼續(xù)在后面讀取,否則忽略
          if (!resource.exists() && reference.isSkippable()) {
              logSkippingResource(reference);
              return Collections.emptyList();
          }
          return Collections.singletonList(createConfigResourceLocation(reference, resource));
      }
      

      因為 application-DEV.yml 是放在classpath下,在這加一個條件斷點,只關注application-DEV.yml

      reference.getResourceLocation().equals("classpath:/application-DEV.yml");
      

      判斷文件是否存在這個語句執(zhí)行的結果是存在的:

      可見spring.profiles.active的值被設置成了DEV,本地在 idea 中 debug 項目代碼也能正常加載 application-dev.yml ,會不會是打成 jar 包之后就不行呢?

      遠程調試生產(chǎn)包

      idea 支持** Remote JVM Debug** ,我想要觀測下生產(chǎn)版本 jar 包啟動的時候,spring.profiles.active的值被設置成了PRO,這塊代碼判斷 classpath:/application-PRO.yml 文件是否存在的結果。

      在生產(chǎn) jar 包目錄下打開命令行窗口,執(zhí)行以下命令,其中 suspend 需要設置成 y,代表回車執(zhí)行命令需要等到 idea 連接到這個 5005 調試端口之后才繼續(xù)執(zhí)行程序:

      java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 -Dapollo.meta=http://10.100.x.xx:8072 -jar app-xxx-web.jar --logging.level.root=TRACE
      

      因為是生產(chǎn)包,所以需要改下條件斷點為:

      reference.getResourceLocation().equals("classpath:/application-PRO.yml");
      

      Remote JVM Debug 啟動后 , 判斷 classpath:/application-PRO.yml 文件是否存在的結果為false,和 debug 項目代碼不一樣了:

      改成 classpath:/application-pro.yml 呢?在斷點處執(zhí)行以下命令,判斷結果為 true了:

      解決方案

      分析到這,問題點和解決方案已經(jīng)出來了:

      1. 項目 pom 文件中 profile 設置的 env 參數(shù)值本該小寫但是用了大寫
      2. 全面使用 apollo,去掉不同環(huán)境的配置文件,修正 apollo 命名空間配置為: apollo.bootstrap.namespaces = application.yml

      可為什么測試環(huán)境沒有出現(xiàn)這個問題呢,原來測試環(huán)境的啟動腳本中指定了spring.profiles.active的值且是小寫,生產(chǎn)環(huán)境啟動腳本卻沒有指定。

      ??

      深入認識 ClassLoader

      不一樣的 ClassLoader

      版本能正常投產(chǎn)了,但是同一份代碼不同的啟動方式卻有不同的表現(xiàn),這著實讓我費解,想著空余花點時間來弄明白這其中的原理。

      resolveNonPattern 這個方法里面調用了resource.exists()方法判斷配置文件是否存在,resource 是 spring-core 提供的 org.springframework.core.io.ClassPathResource 類:

      /**
       * {@link Resource} implementation for class path resources. Uses either a
       * given {@link ClassLoader} or a given {@link Class} for loading resources.
       *
       * <p>Supports resolution as {@code java.io.File} if the class path
       * resource resides in the file system, but not for resources in a JAR.
       * Always supports resolution as {@code java.net.URL}.
       *
       * ...
       */
      public class ClassPathResource extends AbstractFileResolvingResource {
      
      	private final String path;
      
      	@Nullable
      	private ClassLoader classLoader;
      
      	@Nullable
      	private Class<?> clazz;
      
          /**
           * This implementation checks for the resolution of a resource URL.
           * @see ClassLoader#getResource(String)
           * @see Class#getResource(String)
           */
          @Override
          public boolean exists() {
          	return (resolveURL() != null);
          }
      
          /**
           * Resolves a URL for the underlying class path resource.
           * @return the resolved URL, or {@code null} if not resolvable
           */
          @Nullable
          protected URL resolveURL() {
          	try {
          		if (this.clazz != null) {
          			return this.clazz.getResource(this.path);
          		}
          		else if (this.classLoader != null) {
                      // 關鍵行:委托給具體的 ClassLoader
          			return this.classLoader.getResource(this.path);
          		}
          		else {
          			return ClassLoader.getSystemResource(this.path);
          		}
          	}
          	catch (IllegalArgumentException ex) {
          		// Should not happen according to the JDK's contract:
          		// see https://github.com/openjdk/jdk/pull/2662
          		return null;
          	}
          }
      
          ...
      }
      

      2.7.18 版本的 SpringBoot 使用的 spring-core 版本為 5.3.31,exists() 方法會調用本類中的 resolveURL() 方法,debug 看了下,不管是 idea 啟動還是打成 jar 之后啟動,resolveURL 方法中都是通過return this.classLoader.getResource(this.path)返回配置文件的 URL 的,區(qū)別在于:

      • idea 中啟動時,classLoaderLauncher$AppClassLoaderexists方法中return (resolveURL() != null)的返回值為 true
      • 打成 jar 之后啟動,classLoaderLaunchedURLClassLoaderexists方法中return (resolveURL() != null)的返回值為 false
      Launcher$AppClassLoader 與 LaunchedURLClassLoader 的差異
      類加載器的架構差異

      Launcher$AppClassLoader 是 JDK 標準的三層類加載器架構中的系統(tǒng)類加載器:

      // JDK 類加載器層次結構
      Bootstrap ClassLoader (C++實現(xiàn),加載JRE核心類)
          ↓
      Extension ClassLoader (加載JRE擴展包)
          ↓
      AppClassLoader (系統(tǒng)類加載器,加載-classpath指定路徑)
      

      在 idea 環(huán)境中運行時,應用類路徑由 idea 動態(tài)構建,通常包含:

      項目編譯輸出目錄(如target/classes
      所有依賴的 JAR 文件
      idea 特定的資源目錄

      此時的資源查找基于文件系統(tǒng),ClassLoader.getResource()方法會遍歷類路徑中的每個條目,在文件系統(tǒng)上直接查找對應的資源文件。

      LaunchedURLClassLoader 是 SpringBoot 為 FatJar 設計的特殊類加載器:

      // SpringBoot FatJar 類加載架構
      LaunchedURLClassLoader
          ↓
      URLClassLoader
          ↓
      ClassLoader (父類加載器)
      

      其特殊之處在于能夠處理"嵌套的JAR"(nested JARs)——即 FatJar 中內嵌的其他 JAR 文件。

      資源解析機制的對比

      AppClassLoader 的資源解析流程:

      // 簡化版的資源查找邏輯
      public URL getResource(String name) {
          URL url;
          // 首先委托父加載器查找
          if (parent != null) {
              url = parent.getResource(name);
          } else {
              url = getBootstrapResource(name);
          }
          if (url == null) {
              // 在自身的類路徑中查找
              url = findResource(name);
          }
          return url;
      }
      // 在文件系統(tǒng)中的查找
      URL findResource(String name) {
          for (File classpathEntry : classpath) {
              File resourceFile = new File(classpathEntry, name);
              if (resourceFile.exists()) {
                  return resourceFile.toURI().toURL();
              }
          }
          return null;
      }
      

      關鍵特性:

      基于文件系統(tǒng)路徑直接查找
      受操作系統(tǒng)文件系統(tǒng)大小寫規(guī)則影響(Windows 不敏感,Linux 敏感)
      支持通配符和模式匹配

      LaunchedURLClassLoader 的資源解析流程:

      // SpringBoot 自定義的資源查找
      public URL findResource(String name) {
          // 1. 首先嘗試從已索引的資源中查找
          URL url = findResourceFromIndex(name);
          if (url != null) {
              return url; 
          }
          // 2. 在嵌套的JAR文件中查找
          for (JarFile jar : nestedJars) {
              JarEntry entry = jar.getJarEntry(name);
              if (entry != null) {
                  try {
                      // 創(chuàng)建特殊的URL指向JAR內的資源
                      return createJarUrl(jar, entry);
                  } catch (IOException e) {
                      // 處理異常
                  }
              }
          }
          // 3. 回退到標準的URLClassLoader查找
          return super.findResource(name);
      }
      

      關鍵特性:

      基于 JAR 文件條目的精確匹配
      嚴格的大小寫敏感性(ZIP/JAR實現(xiàn)的實際要求)
      需要預先構建資源索引以提高性能

      FatJar 中的資源定位機制

      SpringBoot FatJar 的特殊結構:

      app.jar
      ├── META-INF/
      ├── BOOT-INF/
      │   ├── classes/          # 應用類文件
      │   │   └── application.yml
      │   └── lib/              # 依賴庫
      │       ├── spring-core.jar
      │       └── druid.jar
      └── org/springframework/boot/loader/
          ├── Launcher.class
          └── LaunchedURLClassLoader.class
      

      資源解析的核心挑戰(zhàn):

      1. JAR 規(guī)范的限制

      SpringBoot 的優(yōu)化策略

      // SpringBoot 在構建時創(chuàng)建資源索引
      private Map<String, List<String>> createResourceIndex() {
          Map<String, List<String>> index = new HashMap<>();
          for (JarFile jar : getAllJars()) {
              Enumeration<JarEntry> entries = jar.entries();
              while (entries.hasMoreElements()) {
                  JarEntry entry = entries.nextElement();
                  if (!entry.isDirectory()) {
                      String path = entry.getName();
                      // 將路徑轉換為標準形式
                      String normalized = normalizePath(path);
                      index.computeIfAbsent(normalized, k -> new ArrayList<>())
                      .add(path);
                  }
              }
          }
          return index;
      }
      
      1. 類加載器的初始化差異
        • idea 環(huán)境:類路徑包含具體的目錄和文件
        • FatJar 環(huán)境:類路徑指向 JAR 文件內部的嵌套結構
      操作系統(tǒng)的影響

      開發(fā)環(huán)境(Windows):

      // 文件系統(tǒng)級別的大小寫處理
      File file = new File("application-PRO.yml");
      System.out.println(file.exists()); 
      // Windows: 如果存在application-pro.yml,可能返回true(不敏感)
      // Linux: 嚴格返回false(敏感)
      

      生產(chǎn)環(huán)境(Linux):

      // JAR文件內部的資源查找
      JarFile jar = new JarFile("app.jar");
      JarEntry entry1 = jar.getJarEntry("application-PRO.yml"); // null
      JarEntry entry2 = jar.getJarEntry("application-pro.yml"); // 找到條目
      
      SpringBoot 配置加載的完整鏈條

      理解整個配置加載過程中 ClassLoader 的作用:

      // 配置解析的完整調用鏈
      ConfigDataEnvironmentPostProcessor.postProcessEnvironment()
        → ConfigDataEnvironment.processAndApply()
          → ConfigDataImporter.resolveAndLoad()
            → StandardConfigDataLocationResolver.resolve()
              → ClassPathResource.exists()
                → LaunchedURLClassLoader.getResource()
                  → JarFile.getJarEntry() // 嚴格大小寫匹配
      

      關鍵發(fā)現(xiàn):

      • 在 FatJar 中,資源查找最終委托給java.util.jar.JarFile
      • JarFile.getJarEntry()方法基于哈希表實現(xiàn),要求精確的鍵匹配
      • 哈希鍵的計算基于原始字節(jié),不進行大小寫轉換
      問題復現(xiàn)的技術根源

      通過源碼分析,可以精確重現(xiàn)問題:

      // 問題重現(xiàn)的偽代碼
      public class ProblemReproduction {
          public static void main(String[] args) {
              // 開發(fā)環(huán)境(IDE)
              ClassLoader devLoader = Launcher.AppClassLoader;
              URL devResource = devLoader.getResource("application-PRO.yml");
              System.out.println("DEV Found: " + (devResource != null)); // true
      
              // 生產(chǎn)環(huán)境(FatJar)
              ClassLoader prodLoader = new LaunchedURLClassLoader();
              URL prodResource = prodLoader.getResource("application-PRO.yml");
              System.out.println("PROD Found: " + (prodResource != null)); // false
      
              // 實際存在的文件
              URL actualResource = prodLoader.getResource("application-pro.yml");
              System.out.println("Actual Found: " + (actualResource != null)); // true
          }
      }
      
      設計啟示與最佳實踐

      架構層面的啟示:

      1. 環(huán)境一致性:開發(fā)、測試、生產(chǎn)環(huán)境的運行時行為應該盡可能一致
      2. 早期驗證:在構建階段就應該檢測配置文件和類路徑的一致性
      3. 防御性編程:對資源加載進行適當?shù)娜蒎e處理

      技術實踐建議:

      // 防御性的資源配置加載
      public class SafeConfigLoader {
          public static Resource loadConfig(ClassLoader loader, String baseName,  String profile) {
              // 嘗試規(guī)范化的命名
              String[] possibleNames = {
                  baseName + "-" + profile.toLowerCase() + ".yml",
                  baseName + "-" + profile.toUpperCase() + ".yml",
                  baseName + "-" + profile + ".yml"
              };
      
              for (String name : possibleNames) {
                  Resource resource = loader.getResource(name);
                  if (resource != null && resource.exists()) {
                      return resource;
                  }
              }
              return null;
          }
      }
      

      構建期檢查:

      <!-- Maven 構建期資源驗證 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-enforcer-plugin</artifactId>
        <configuration>
          <rules>
            <requireFilesExist>
              <files>
                <!-- 驗證配置文件命名一致性 -->
                <file>src/main/resources/application-${env}.yml</file>
              </files>
            </requireFilesExist>
          </rules>
        </configuration>
      </plugin>
      
      posted @ 2025-10-21 20:10  杜勁松  閱讀(23)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 手机| 久久综合亚洲色一区二区三区| 国产地址二永久伊甸园| 亚洲中文字幕第二十三页| 最新亚洲av日韩av二区| 26uuu另类亚洲欧美日本| 国产免费无遮挡吸乳视频在线观看| 无码专区视频精品老司机| 国产亚洲精品AA片在线爽| 国产91丝袜在线播放动漫| 成人午夜看黄在线尤物成人| 夜夜躁狠狠躁日日躁| 日韩亚洲欧美中文高清| 亚洲综合色一区二区三区| 国产综合久久久久鬼色| 中文字幕在线视频不卡一区二区 | 欧美成人精品| 亚洲男人的天堂久久香蕉| 久久精品丝袜高跟鞋| 免费观看性行为视频的网站| 国产成人精品国产成人亚洲| 女性高爱潮视频| 国产成AV人片久青草影院| 国产美女精品一区二区三区| 国产亚洲av夜间福利香蕉149| 一本大道久久香蕉成人网| 无码高潮爽到爆的喷水视频app| 亚洲精品国产精品国在线| 在线 欧美 中文 亚洲 精品| 欧美亚洲另类 丝袜综合网| 四虎永久在线精品8848a| 成熟了的熟妇毛茸茸| 91精品久久一区二区三区| 午夜毛片不卡免费观看视频| 综合激情亚洲丁香社区| 国产伦一区二区三区久久| 亚洲国产日韩a在线播放| 色综合天天综合天天更新| 成人av午夜在线观看| 男女动态无遮挡动态图| 亚洲国产精品男人的天堂|