修改包名触发StringIndexOutOfBoundsException,读取随机目录.class注解遇阻求助
首先咱们先定位你遇到的异常根源:你的getClassWithPackageFromPath方法里硬编码了packageStartsFrom = "com.",但随机目录下的类文件路径根本不一定包含这个前缀,导致indexOf("com.")返回-1,再调用substring(-1)直接触发了字符串越界异常。更关键的是,从文件路径推导类的包名本身就不靠谱——类的实际包名是编译时写入字节码的,和文件当前所在的目录结构没有必然联系,尤其是随机放置的情况,路径和包名完全可能不匹配,所以这个思路从一开始就有问题。
给你两个靠谱的修复方案,按需选择:
方案一:直接读取.class字节码获取注解(推荐)
既然包名和注解都存在于.class文件的字节码中,咱们完全可以跳过路径推导这一步,直接解析字节码来获取信息。这里推荐用ASM库(轻量、高效,很多框架都在用),不用费劲折腾类加载器。
步骤1:引入ASM依赖(如果用Maven)
<dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>9.5</version> </dependency>
步骤2:编写字节码解析代码
替换你原来的getFixFromClassFile方法,用下面这段代码:
import org.objectweb.asm.ClassReader; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Opcodes; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; private String[] getFixAuthorsFromClassFile(Path pathToClass) throws IOException { List<String> authorList = new ArrayList<>(); // 读取.class文件的输入流 try (FileInputStream fis = new FileInputStream(pathToClass.toFile())) { ClassReader classReader = new ClassReader(fis); // 访问类上的注解,跳过不需要的代码、调试信息等,提升效率 classReader.accept(new AnnotationVisitor(Opcodes.ASM9) { @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { // 替换成你Fix注解的全限定名对应的descriptor(把.换成/,前面加L,后面加;) if ("Lcom/yourpackage/Fix;".equals(descriptor)) { // 访问Fix注解的属性 return new AnnotationVisitor(Opcodes.ASM9) { @Override public AnnotationVisitor visitArray(String name) { // 如果author是数组类型,处理数组元素 if ("author".equals(name)) { return new AnnotationVisitor(Opcodes.ASM9) { @Override public void visit(String arrayItemName, Object value) { authorList.add((String) value); } }; } return super.visitArray(name); } @Override public void visit(String name, Object value) { // 如果author是单个字符串类型,直接取value if ("author".equals(name)) { authorList.add((String) value); } } }; } return super.visitAnnotation(descriptor, visible); } }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); } return authorList.toArray(new String[0]); }
这个方法的好处是完全不依赖文件路径,不管类文件扔在哪个目录,都能直接从字节码里读出注解信息,彻底解决路径推导包名的问题。
方案二:改进包名推导逻辑(仅当目录结构和包结构一致时可用)
如果你坚持想用类加载器的方式,那得先保证类文件的目录结构和它的包名完全匹配,然后改进getClassWithPackageFromPath方法,去掉硬编码的包前缀,改用相对于根目录的路径来推导:
private String getClassWithPackageFromPath(Path pathToClass, Path baseDir) { final String classFileExtension = ".class"; // 获取类文件相对于根目录的路径 Path relativePath = baseDir.relativize(pathToClass); // 把路径分隔符换成点,去掉.class后缀 return relativePath.toString() .replace(File.separator, ".") .replace(classFileExtension, ""); }
调用的时候传入你的根目录folder作为baseDir:
String classWithPackage = getClassWithPackageFromPath(pathToClass, folder.toPath());
但要注意:这个方法只有当你的类文件的目录结构和包名完全一致时才有效(比如com/example/Test.class对应com.example.Test),如果是随机乱放的类文件,这个方法依然会导致类加载失败。
额外的异常处理优化
不管用哪种方案,都要给关键步骤加上异常处理,避免类似的越界异常再次出现。比如在原来的方法里,先判断indexOf的结果:
private String getClassWithPackageFromPath(Path pathToClass) { final String packageStartsFrom = "com."; final String classFileExtension = ".class"; final String pathWithDots = pathToClass.toString().replace(File.separator, "."); int startIndex = pathWithDots.indexOf(packageStartsFrom); if (startIndex == -1) { throw new IllegalArgumentException("当前类文件路径不包含预期的包前缀: " + packageStartsFrom); // 或者返回一个明确的标识,比如null,然后在调用处处理 // return null; } return pathWithDots.substring(startIndex).replace(classFileExtension, ""); }
总的来说,方案一是最可靠的,因为它直接从类的字节码源头获取信息,不受文件存放位置的影响。
内容的提问来源于stack exchange,提问作者Peter Penzov

