修复MultiResource游标歧义问题(issue#IDNAOY@Gitee)

This commit is contained in:
Looly
2026-02-01 20:05:26 +08:00
parent 4938242c9a
commit 1317ebba97
17 changed files with 332 additions and 31 deletions

View File

@@ -57,7 +57,7 @@ public class ResourceClassLoader<T extends Resource> extends SecureClassLoader {
* @return this
*/
public ResourceClassLoader<T> addResource(final T resource) {
this.resourceMap.put(resource.getName(), resource);
this.resourceMap.put(resource.name(), resource);
return this;
}

View File

@@ -432,7 +432,7 @@ public class ZipUtil {
*
* @param zipFile 生成的Zip文件包括文件名。注意zipPath不能是srcPath路径下的子文件夹
* @param charset 编码
* @param resources 需要压缩的资源,资源的路径为{@link Resource#getName()}
* @param resources 需要压缩的资源,资源的路径为{@link Resource#name()}
* @return 压缩文件
* @throws HutoolException IO异常
* @since 5.5.2

View File

@@ -189,14 +189,14 @@ public class ZipWriter implements Closeable {
/**
* 添加资源到压缩包,添加后关闭资源流
*
* @param resources 需要压缩的资源,资源的路径为{@link Resource#getName()}
* @param resources 需要压缩的资源,资源的路径为{@link Resource#name()}
* @return this
* @throws IORuntimeException IO异常
*/
public ZipWriter add(final Resource... resources) throws IORuntimeException {
for (final Resource resource : resources) {
if (null != resource) {
add(resource.getName(), resource.getStream());
add(resource.name(), resource.getStream());
}
}
return this;

View File

@@ -395,7 +395,7 @@ public class DelegatePath extends SimpleWrapper<Path> implements Path, Resource
}
@Override
public String getName() {
public String name() {
return PathUtil.getName(this.raw);
}

View File

@@ -67,7 +67,7 @@ public class BytesResource implements Resource, Serializable {
}
@Override
public String getName() {
public String name() {
return this.name;
}

View File

@@ -57,7 +57,7 @@ public class FileObjectResource implements Resource {
}
@Override
public String getName() {
public String name() {
return this.fileObject.getName();
}

View File

@@ -94,7 +94,7 @@ public class FileResource implements Resource, Serializable {
// ----------------------------------------------------------------------- Constructor end
@Override
public String getName() {
public String name() {
return this.name;
}

View File

@@ -54,8 +54,8 @@ public class HttpResource implements Resource, Serializable {
}
@Override
public String getName() {
return resource.getName();
public String name() {
return resource.name();
}
@Override

View File

@@ -74,7 +74,7 @@ public class InputStreamResource implements Resource, Serializable {
}
@Override
public String getName() {
public String name() {
return this.name;
}

View File

@@ -48,7 +48,7 @@ public class MultiResource implements Resource, Iterable<Resource>, Iterator<Res
/**
* 游标
*/
private int cursor;
private int cursor = -1;
/**
* 构造
@@ -73,48 +73,48 @@ public class MultiResource implements Resource, Iterable<Resource>, Iterator<Res
}
@Override
public String getName() {
return resources.get(cursor).getName();
public String name() {
return resources.get(getValidCursor()).name();
}
@Override
public URL getUrl() {
return resources.get(cursor).getUrl();
return resources.get(getValidCursor()).getUrl();
}
@Override
public long size() {
return resources.get(cursor).size();
return resources.get(getValidCursor()).size();
}
@Override
public InputStream getStream() {
return resources.get(cursor).getStream();
return resources.get(getValidCursor()).getStream();
}
@Override
public boolean isModified() {
return resources.get(cursor).isModified();
return resources.get(getValidCursor()).isModified();
}
@Override
public BufferedReader getReader(final Charset charset) {
return resources.get(cursor).getReader(charset);
return resources.get(getValidCursor()).getReader(charset);
}
@Override
public String readStr(final Charset charset) throws IORuntimeException {
return resources.get(cursor).readStr(charset);
return resources.get(getValidCursor()).readStr(charset);
}
@Override
public String readUtf8Str() throws IORuntimeException {
return resources.get(cursor).readUtf8Str();
return resources.get(getValidCursor()).readUtf8Str();
}
@Override
public byte[] readBytes() throws IORuntimeException {
return resources.get(cursor).readBytes();
return resources.get(getValidCursor()).readBytes();
}
@Override
@@ -138,14 +138,14 @@ public class MultiResource implements Resource, Iterable<Resource>, Iterator<Res
@Override
public void remove() {
this.resources.remove(this.cursor);
this.resources.remove(getValidCursor());
}
/**
* 重置游标
*/
public synchronized void reset() {
this.cursor = 0;
this.cursor = -1;
}
/**
@@ -171,4 +171,12 @@ public class MultiResource implements Resource, Iterable<Resource>, Iterator<Res
return this;
}
/**
* 获取当前有效游标位置的资源
*
* @return 资源
*/
private int getValidCursor() {
return Math.max(cursor, 0);
}
}

View File

@@ -53,7 +53,7 @@ public interface Resource {
* @return 资源名
* @since 4.0.13
*/
String getName();
String name();
/**
* 获得解析后的{@link URL}无对应URL的返回{@code null}

View File

@@ -319,7 +319,7 @@ public class ResourceUtil {
public static void loadTo(final Properties properties, final Resource resource, final Charset charset) {
Assert.notNull(properties);
Assert.notNull(resource);
final String filename = resource.getName();
final String filename = resource.name();
if (filename != null && StrUtil.endWithIgnoreCase(filename, ".xml")) {
// XML
try (final InputStream in = resource.getStream()) {

View File

@@ -89,7 +89,7 @@ public class UrlResource implements Resource, Serializable {
//-------------------------------------------------------------------------------------- Constructor end
@Override
public String getName() {
public String name() {
return this.name;
}

View File

@@ -86,7 +86,7 @@ public class VfsResource implements Resource {
}
@Override
public String getName() {
public String name() {
return MethodUtil.invoke(virtualFile, VIRTUAL_FILE_METHOD_GET_NAME);
}

View File

@@ -0,0 +1,293 @@
/*
* Copyright (c) 2026 Hutool Team.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hutool.v7.core.io.resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* MultiResource Iterator接口功能单元测试
*/
class MultiResourceTest {
private MultiResource multiResource;
@BeforeEach
void setUp() {
// 创建具体的Resource实现用于测试
final TestResource testResource1 = new TestResource("resource1", "content1");
final TestResource testResource2 = new TestResource("resource2", "content2");
final TestResource testResource3 = new TestResource("resource3", "content3");
final List<Resource> resourceList = new ArrayList<>();
resourceList.add(testResource1);
resourceList.add(testResource2);
resourceList.add(testResource3);
multiResource = new MultiResource(resourceList);
}
/**
* 测试hasNext方法 - 初始状态
*/
@Test
void testHasNextInitial() {
// 初始游标为-1有效游标为0所以应该有下一个元素
assertTrue(multiResource.hasNext(), "初始状态应该有下一个元素");
}
/**
* 测试hasNext方法 - 中间状态
*/
@Test
void testHasNextMiddle() {
// 调用一次next()后游标变为0还有剩余元素
multiResource.next();
assertTrue(multiResource.hasNext(), "中间状态应该还有下一个元素");
// 再调用两次next()游标变为2还应有下一个元素
multiResource.next();
multiResource.next();
assertTrue(multiResource.hasNext(), "接近末尾时应该还有下一个元素");
}
/**
* 测试hasNext方法 - 末尾状态
*/
@Test
void testHasNextEnd() {
// 连续调用next()直到超出范围
multiResource.next(); // 游标: 0
multiResource.next(); // 游标: 1
multiResource.next(); // 游标: 2
multiResource.next(); // 游标: 3
assertFalse(multiResource.hasNext(), "超出范围后不应该有下一个元素");
}
/**
* 测试next方法 - 正常遍历
*/
@Test
void testNextNormalTraversal() {
// 第一次调用next()
final Resource result1 = multiResource.next();
assertEquals(multiResource, result1, "next()返回的应该是自身实例");
assertEquals(0, getCursorValue(multiResource), "游标应该更新为0");
// 验证getName方法使用的是第一个资源
assertEquals("resource1", multiResource.name(), "getName应该返回第一个资源名称");
// 第二次调用next()
final Resource result2 = multiResource.next();
assertEquals(multiResource, result2, "next()返回的应该是自身实例");
assertEquals(1, getCursorValue(multiResource), "游标应该更新为1");
// 验证getName方法使用的是第二个资源
assertEquals("resource2", multiResource.name(), "getName应该返回第二个资源名称");
}
/**
* 测试next方法 - 超出边界异常
*/
@Test
void testNextBoundaryException() {
// 遍历所有资源
multiResource.next(); // 游标: 0
multiResource.next(); // 游标: 1
multiResource.next(); // 游标: 2
multiResource.next(); // 游标: 3
// 再次调用next()应该抛出异常
assertThrows(ConcurrentModificationException.class, () -> {
multiResource.next();
}, "超出边界时调用next()应该抛出ConcurrentModificationException");
}
/**
* 测试remove方法
*/
@Test
void testRemove() {
// 先移动游标到第一个位置
multiResource.next(); // 游标: 0
assertEquals(3, getInternalSize(multiResource), "初始资源数量应该是3");
// 执行删除操作
multiResource.remove();
assertEquals(2, getInternalSize(multiResource), "删除后资源数量应该是2");
assertEquals("resource2", multiResource.name(), "删除后getName应该返回第二个资源名称");
}
/**
* 测试iterator方法
*/
@Test
void testIterator() {
final var iterator = multiResource.iterator();
assertNotNull(iterator, "iterator方法不应返回null");
// 验证迭代器可以遍历所有资源
int count = 0;
while (iterator.hasNext()) {
final Resource resource = iterator.next();
assertNotNull(resource, "迭代器返回的资源不应为null");
count++;
}
assertEquals(3, count, "迭代器应该能遍历所有3个资源");
}
/**
* 测试reset方法
*/
@Test
void testReset() {
// 移动游标到中间位置
multiResource.next(); // 游标: 0
multiResource.next(); // 游标: 1
assertEquals(1, getCursorValue(multiResource), "游标应该为1");
// 重置游标
multiResource.reset();
assertEquals(-1, getCursorValue(multiResource), "重置后游标应该为-1");
// 重置后应该能够重新开始遍历
assertTrue(multiResource.hasNext(), "重置后应该有下一个元素");
final Resource result = multiResource.next();
assertEquals(multiResource, result, "重置后next()应该返回自身");
assertEquals(0, getCursorValue(multiResource), "重置后第一次next()后游标应该为0");
}
/**
* 测试并发安全的同步方法
*/
@Test
void testSynchronizedMethods() {
// 由于无法直接测试同步块,我们验证方法可以正常调用
assertDoesNotThrow(() -> {
multiResource.reset();
}, "reset方法应该可以正常调用");
assertDoesNotThrow(() -> {
multiResource.next();
}, "next方法应该可以正常调用");
}
/**
* 测试添加资源后的迭代行为
*/
@Test
void testAddResourceThenIterate() {
final TestResource newResource = new TestResource("newResource", "newContent");
// 添加新资源
multiResource.add(newResource);
// 验证可以遍历新增的资源
// 先遍历原有的3个资源
multiResource.next(); // 游标: 0
multiResource.next(); // 游标: 1
multiResource.next(); // 游标: 2
multiResource.next(); // 游标: 3
// 现在应该还有元素第4个
assertTrue(multiResource.hasNext(), "添加资源后应该还有元素");
multiResource.next(); // 游标: 4
assertEquals(4, getInternalSize(multiResource), "资源总数应该是4");
}
/**
* 测试addAll方法
*/
@Test
void testAddAllResources() {
final TestResource extraResource1 = new TestResource("extra1", "extraContent1");
final TestResource extraResource2 = new TestResource("extra2", "extraContent2");
final List<Resource> extraResources = List.of(extraResource1, extraResource2);
// 添加多个资源
multiResource.addAll(extraResources);
assertEquals(5, getInternalSize(multiResource), "添加多个资源后总数应该是5");
}
// 辅助方法:通过反射获取私有字段值
private int getCursorValue(final MultiResource multiResource) {
try {
final var field = MultiResource.class.getDeclaredField("cursor");
field.setAccessible(true);
return field.getInt(multiResource);
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
private int getInternalSize(final MultiResource multiResource) {
try {
final var field = MultiResource.class.getDeclaredField("resources");
field.setAccessible(true);
final List<?> resources = (List<?>) field.get(multiResource);
return resources.size();
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
/**
* 测试Resource接口的实现类用于单元测试
*/
private record TestResource(String name, String content) implements Resource {
@Override
public URL getUrl() {
try {
return new URL("http://example.com/" + name);
} catch (final MalformedURLException e) {
throw new RuntimeException(e);
}
}
@Override
public long size() {
return content.getBytes(StandardCharsets.UTF_8).length;
}
@Override
public InputStream getStream() {
return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
}
@Override
public boolean isModified() {
return false;
}
}
}

View File

@@ -38,7 +38,7 @@ public class ResourceUtilTest {
@Test
public void stringResourceTest(){
final StringResource stringResource = new StringResource("testData", "test");
Assertions.assertEquals("test", stringResource.getName());
Assertions.assertEquals("test", stringResource.name());
Assertions.assertArrayEquals("testData".getBytes(), stringResource.readBytes());
Assertions.assertArrayEquals("testData".getBytes(), IoUtil.readBytes(stringResource.getStream()));
}
@@ -46,7 +46,7 @@ public class ResourceUtilTest {
@Test
public void fileResourceTest(){
final FileResource resource = new FileResource(FileUtil.file("test.xml"));
Assertions.assertEquals("test.xml", resource.getName());
Assertions.assertEquals("test.xml", resource.name());
Assertions.assertTrue(StrUtil.isNotEmpty(resource.readUtf8Str()));
}

View File

@@ -162,7 +162,7 @@ public class MultipartOutputStream extends OutputStream {
* @throws IORuntimeException IO异常
*/
private void appendResource(final String formFieldName, final Resource resource) throws IORuntimeException {
final String fileName = resource.getName();
final String fileName = resource.name();
// Content-Disposition
if (null == fileName) {