import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; /** * 在 Spine 3.8.99 的 launcher.jar 里插一条调用: * com.esotericsoftware.spine.editor.launcher.Launcher.main(String[]) * 的第一行改成先调 com.spineview.SceneScan.main(String[]), * 并把我们编译好的类一起打进 jar。 * * 因为 3.8.99 是明文的,这么做之后我们的代码就和编辑器处在同一个类加载器里, * 不再有"跨加载器看不到 TextButton"的问题。 */ public final class Patcher { /** 目标类 / 方法 / 描述符,从命令行传(args[3..5])。 */ private static String targetClass; private static String targetEntry; private static String hookMethod; private static String hookDesc; private static final String HOOK_OWNER = "com/spineview/SceneScan"; public static void main(String[] args) throws Exception { Path source = Paths.get(args[0]); Path target = Paths.get(args[1]); Path classesDir = Paths.get(args[2]); targetClass = args.length > 3 ? args[3] : "com/esotericsoftware/spine/editor/launcher/Launcher"; String methodName = args.length > 4 ? args[4] : "main"; hookMethod = args.length > 5 ? args[5] : "main"; hookDesc = args.length > 6 ? args[6] : "([Ljava/lang/String;)V"; targetEntry = targetClass + ".class"; Map entries = new LinkedHashMap(); try (JarInputStream in = new JarInputStream(Files.newInputStream(source))) { JarEntry entry; while ((entry = in.getNextJarEntry()) != null) { if (entry.isDirectory()) continue; entries.put(entry.getName(), readAll(in)); } } System.out.println("原 jar 条目数 = " + entries.size()); byte[] original = entries.get(targetEntry); if (original == null) throw new IllegalStateException("找不到 " + targetEntry); entries.put(targetEntry, patch(original, methodName)); System.out.println("已给 " + targetClass + "." + methodName + " 插入了 " + HOOK_OWNER + "." + hookMethod); int added = 0; try (java.util.stream.Stream stream = Files.walk(classesDir)) { for (Path path : (Iterable) stream::iterator) { if (!Files.isRegularFile(path)) continue; String relative = classesDir.relativize(path).toString(); if (!relative.endsWith(".class")) continue; entries.put(relative.replace('\\', '/'), Files.readAllBytes(path)); added++; } } System.out.println("加进 jar 的类 = " + added); try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) { for (Map.Entry entry : entries.entrySet()) { JarEntry jarEntry = new JarEntry(entry.getKey()); jarEntry.setTime(0L); out.putNextEntry(jarEntry); out.write(entry.getValue()); out.closeEntry(); } } System.out.println("已写出 " + target); } private static byte[] patch(byte[] original, final String methodName) { ClassReader reader = new ClassReader(original); ClassWriter writer = new ClassWriter(0); ClassVisitor visitor = new ClassVisitor(Opcodes.ASM9, writer) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor next = super.visitMethod(access, name, descriptor, signature, exceptions); if (!methodName.equals(name) || !hookDesc.equals(descriptor)) return next; return new MethodVisitor(Opcodes.ASM9, next) { @Override public void visitCode() { super.visitCode(); // 我们的钩子是 ()V,不需要压任何参数;栈平衡保持不变。 visitMethodInsn(Opcodes.INVOKESTATIC, HOOK_OWNER, hookMethod, hookDesc, false); } }; } }; reader.accept(visitor, 0); return writer.toByteArray(); } private static byte[] readAll(InputStream in) throws Exception { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] chunk = new byte[16384]; int read; while ((read = in.read(chunk)) > 0) buffer.write(chunk, 0, read); return buffer.toByteArray(); } }