Spring-boot-loader中JarFileWrapper包装类的设计目的是什么?
背景
我在复用spring-boot-loader的Jar包相关代码时,发现Spring自定义的CustomJarFile重写了close()方法:
@Override public void close() throws IOException { if (this.closed) { return; } super.close(); if (this.type == JarFileType.DIRECT) { this.rootFile.close(); } this.closed = true; }
同时Spring还定义了一个未重写close方法的JarFileWrapper类,自定义的JarUrlConnection#get()方法返回的是包含这个包装类的实例,而非CustomJarFile本身:
static JarURLConnection get(URL url, JarFile jarFile) throws IOException { // 省略其他逻辑... return new JarURLConnection(url, jarFile.getWrapper(), jarEntryName); // 这里的JarFile是自定义的CustomJarFile }
测试时如果直接传入CustomJarFile实例而非包装类,会触发"zip file closed"异常。原因是调用包装类的close()只会执行父类ZipFile的close(),但调用CustomJarFile的close()会执行上面那段重写的逻辑。JarFileWrapper的JavaDoc说明是:"用于创建CustomJarFile的副本,可安全关闭而不影响原实例。"
问题
我搞不懂这里用包装类的意义在哪。如果只是为了避免关闭CustomJarFile,直接不在CustomJarFile里重写close()方法,让它默认调用ZipFile的close()不就行了?感觉这多了一层冗余。而且我调试时发现,从服务启动到关闭,CustomJarFile的close()根本没被调用过,这就让我更觉得如果去掉重写的close(),包装类完全没用了。
这得从Spring Boot Loader的核心设计逻辑说起:
CustomJarFile的生命周期管控
Spring Boot Loader里的CustomJarFile不只是普通Jar文件的包装,它负责处理嵌套Jar、分层Jar这类特殊场景,整个应用的类加载、资源读取都依赖它保持打开状态。如果直接让它继承ZipFile的默认close(),一旦第三方代码(比如JDK自带的JarURLConnection相关逻辑)调用了这个方法,就会把底层的文件句柄关掉,导致后续加载资源时触发"zip file closed"异常。JarFileWrapper的隔离作用
JDK的JarURLConnection有个特性:当连接关闭时,会自动关闭关联的JarFile。如果直接把CustomJarFile传给它,连接关闭时就会触发CustomJarFile重写的close(),进而关掉关联的rootFile,这会直接破坏整个应用的类加载能力。而JarFileWrapper作为无修改的包装类,它的close()只会关闭自己的包装层,不会触动CustomJarFile的核心资源,这样就能隔离外部代码的关闭操作,保证CustomJarFile始终处于可用状态。为什么不直接去掉CustomJarFile的close()重写?
重写close()是为了在应用完全终止的收尾阶段,能统一清理所有关联的文件句柄(比如rootFile)。你调试时没看到它被调用,是因为服务运行期间根本不该调用它——它的关闭是由Spring Boot的生命周期管理统一触发的,属于应用终止时的最后一步清理,不是随便哪个地方能调用的。
简单说,JarFileWrapper是用来“挡枪”的:让外部代码随便关包装类,不会影响核心的CustomJarFile;而CustomJarFile自己的close()是留着最后统一收尾用的,两者各司其职,完全不是冗余设计。
内容的提问来源于stack exchange,提问作者Miss Skooter

