Spring加载bean配置文件的schemas文件(懒加载)

曾经终败给现在 2021-09-28 20:46 774阅读 0赞

最近在调试Spring源码时,看到加载Spring的xml文件-schema部分,

里面使用了懒加载的写法(PluggableSchemaResolver的getSchemaMappings方法)

为了能更好理解这个懒加载(里面获取到的Map是单例的),我依葫芦画瓢,写了一个简化版的懒汉式单例(采用双重检查锁)。

下面是类以及单元测试

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. /**
  4. * Created by shucheng on 2019-5-31 上午 10:24
  5. */
  6. public class MySchema {
  7. public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";
  8. private final ClassLoader classLoader;
  9. private String schemaMappingsLocation;
  10. private Map<String, String> schemaMappings;
  11. public MySchema(ClassLoader classLoader) {
  12. this.classLoader = classLoader;
  13. this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
  14. }
  15. public Map<String, String> getSchemaMappings() {
  16. Map<String, String> schemaMappings = this.schemaMappings;
  17. // 如果schemaMappings不为null,则直接返回数据,不需要重新创建对象
  18. if (schemaMappings == null) {
  19. // System.out.println("线程" + Thread.currentThread().getName() + "进入if判断");
  20. synchronized (this) {
  21. // System.out.println("线程" + Thread.currentThread().getName() + "进入同步锁");
  22. // System.out.println("线程" + Thread.currentThread().getName() + "\n" +
  23. "schemaMappings=====" + (schemaMappings != null ? schemaMappings.hashCode() : null) + "\n" +
  24. "this.schemaMappings=====" + (this.schemaMappings != null ? this.schemaMappings.hashCode() : null));
  25. schemaMappings = this.schemaMappings;
  26. if (schemaMappings == null) {
  27. System.out.println("开始加载schemaMappingsLocation");
  28. Map<String, String> map = new HashMap();
  29. map.put("1", "张三");
  30. map.put("2", "李四");
  31. map.put("3", "王五");
  32. map.put("4", "赵六");
  33. map.put("5", "钱七");
  34. schemaMappings = new HashMap<>();
  35. schemaMappings.putAll(map);
  36. System.out.println("schemaMappingsLocation加载中");
  37. this.schemaMappings = schemaMappings;
  38. System.out.println("schemaMappingsLocation加载完成");
  39. }
  40. }
  41. }
  42. // System.out.println("线程" + Thread.currentThread().getName() + "======class为" + schemaMappings.getClass().hashCode() + "=====" + schemaMappings);
  43. return schemaMappings;
  44. }
  45. @Override
  46. public String toString() {
  47. return "MySchema{" +
  48. "schemaMappings=" + getSchemaMappings() +
  49. '}';
  50. }
  51. }
  52. import org.junit.Test;
  53. import java.util.Map;
  54. /**
  55. * Created by shucheng on 2019-5-31 上午 10:23
  56. */
  57. public class MyTest {
  58. // 简单测试
  59. @Test
  60. public void test() {
  61. MySchema m = new MySchema(getClass().getClassLoader());
  62. System.out.println(m);
  63. // System.out.println(m.toString());
  64. }
  65. // 模拟多线程(3个线程)测试
  66. // 参考链接:https://www.jb51.net/article/130924.htm
  67. // 另外还参考了MyBatis3.5.1源码里的单元测试org.apache.ibatis.io.VFSTest#getInstanceShouldNotBeNullInMultiThreadEnv
  68. @Test
  69. public void testMultiThread() {
  70. final int threadCount = 20;
  71. Thread[] threads = new Thread[threadCount];
  72. InstanceGetterProcedure procedure = new InstanceGetterProcedure(new MySchema(getClass().getClassLoader()));
  73. for (int i = 0; i < threads.length; i++) {
  74. String threadName = "Thread##" + i;
  75. threads[i] = new Thread(procedure, threadName);
  76. }
  77. for (Thread thread : threads) {
  78. thread.start();
  79. }
  80. }
  81. private class InstanceGetterProcedure implements Runnable {
  82. private MySchema mySchema;
  83. Map<String, String> schemaMappings;
  84. public InstanceGetterProcedure() {
  85. }
  86. public InstanceGetterProcedure(MySchema mySchema) {
  87. this.mySchema = mySchema;
  88. }
  89. @Override
  90. public void run() {
  91. // 获取schemaMappings
  92. schemaMappings = mySchema.getSchemaMappings();
  93. }
  94. }
  95. }

这里我重点说下多线程的情况:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTA5OTk4MDk_size_16_color_FFFFFF_t_70

这个属性被赋过两次值,这点我开始很疑惑,一直想能否把第33行去掉。答案当然是不能去掉(因为去掉的话,多线程情况下有可能返回的不是单例) 根源就是getSchemaMappings方法加锁只是对部分代码加锁,第23行就没有加锁,完全有可能出现多个线程进来后,每个线程的schemaMappings属性都为null

这一点可以通过调用MyTest的testMultiThread单元测试方法来证实,多调用几次就会碰到我所说的情况,下图是我碰到的一次

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTA5OTk4MDk_size_16_color_FFFFFF_t_70 1

去看这个问题的起因:

我之所以会注意到这点,是因为我在用idea调试PluggableSchemaResolver中★★★位置(第93行)时,看到schemaMappings的值为null;但是当这个位置向下再执行一行代码后,鼠标放到schemaMappings上发现已经有结果了。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTA5OTk4MDk_size_16_color_FFFFFF_t_70 2

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTA5OTk4MDk_size_16_color_FFFFFF_t_70 3

20190531145846336.png

这种现象让我感到很奇怪,因为这行代码并没有直接操作schemaMappings属性,本身这个类也没有被代理,只是一个普通方法。

然后我在这个类当中继续搜索schemaMappings,发现有一个getSchemaMappings方法(里面直接操作了schemaMappings属性),而这个方法被两个地方(toString和resolveEntity,后面这个和我看到的现象无关,和实际解析xml时先加载schemas有关)调用了。这时我猜想debug时,idea会监控类以及其中变量,我想到现象是idea监控类的时候调用了toString方法(因为要在Variables显示类的有关信息)

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTA5OTk4MDk_size_16_color_FFFFFF_t_70 4

如果我直接运行单元测试,去掉所有断点,发现在★★★位置后初始化完schemaMappingsLocation变量后,直接在加上System.out.println(“==========================” + schemaMappings);来运行,发现打印出来的是空的

这就证实了我前面猜想的debug过程中idea会调用监控类的toString是对的,所以我一开始看到的现象也就解释得通了

附PuggableSchemaResolver的部分源码:

  1. public class PluggableSchemaResolver implements EntityResolver {
  2. /**
  3. * The location of the file that defines schema mappings.
  4. * Can be present in multiple JAR files.
  5. *
  6. * 默认 {@link #schemaMappingsLocation} 地址
  7. */
  8. public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";
  9. private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class);
  10. @Nullable
  11. private final ClassLoader classLoader;
  12. /**
  13. * Schema文件地址
  14. */
  15. private final String schemaMappingsLocation;
  16. /** Stores the mapping of schema URL -> local schema path. */
  17. @Nullable
  18. private volatile Map<String, String> schemaMappings; // namespaceURI与Schema文件地址的映射集合
  19. /**
  20. * Loads the schema URL -> schema file location mappings using the default
  21. * mapping file pattern "META-INF/spring.schemas".
  22. * @param classLoader the ClassLoader to use for loading
  23. * (can be {@code null}) to use the default ClassLoader)
  24. * @see PropertiesLoaderUtils#loadAllProperties(String, ClassLoader)
  25. */
  26. public PluggableSchemaResolver(@Nullable ClassLoader classLoader) {
  27. this.classLoader = classLoader;
  28. // ★★★
  29. this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
  30. // System.out.println("==========================" + schemaMappings);
  31. }
  32. /**
  33. * Loads the schema URL -> schema file location mappings using the given
  34. * mapping file pattern.
  35. * @param classLoader the ClassLoader to use for loading
  36. * (can be {@code null}) to use the default ClassLoader)
  37. * @param schemaMappingsLocation the location of the file that defines schema mappings
  38. * (must not be empty)
  39. * @see PropertiesLoaderUtils#loadAllProperties(String, ClassLoader)
  40. */
  41. public PluggableSchemaResolver(@Nullable ClassLoader classLoader, String schemaMappingsLocation) {
  42. // 暂时用不到,这里省略具体内容
  43. }
  44. @Override
  45. @Nullable
  46. public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
  47. // 暂时用不到,这里省略具体内容
  48. }
  49. /**
  50. * Load the specified schema mappings lazily.
  51. */
  52. private Map<String, String> getSchemaMappings() {
  53. Map<String, String> schemaMappings = this.schemaMappings;
  54. if (schemaMappings == null) {
  55. synchronized (this) {
  56. schemaMappings = this.schemaMappings;
  57. if (schemaMappings == null) {
  58. // System.out.println("开始加载schemaMappingsLocation");
  59. if (logger.isTraceEnabled()) {
  60. logger.trace("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
  61. }
  62. try {
  63. Properties mappings =
  64. PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
  65. if (logger.isTraceEnabled()) {
  66. logger.trace("Loaded schema mappings: " + mappings);
  67. }
  68. schemaMappings = new ConcurrentHashMap<>(mappings.size());
  69. CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
  70. this.schemaMappings = schemaMappings;
  71. // System.out.println("schemaMappingsLocation加载完成");
  72. }
  73. catch (IOException ex) {
  74. throw new IllegalStateException(
  75. "Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
  76. }
  77. }
  78. }
  79. }
  80. return schemaMappings;
  81. }
  82. @Override
  83. public String toString() {
  84. return "EntityResolver using schema mappings " + getSchemaMappings();
  85. }
  86. }

发表评论

表情:
评论列表 (有 0 条评论,774人围观)

还没有评论,来说两句吧...

相关阅读