字节码导入方案

由于安装插件需要发送字节码,所以我们需要在插件的代码里填入字节码,这就使得我们在开发过程中,插件是插件,远端执行的字节码是字节码,割裂开了项目的一致性。

为了解决在插件开发过程中的割裂性,即插件与远端执行的代码在一个项目中较难同时存在(插件可以使用Java 8编译而远端不一定支持Java 8);需要有一套解决方案。

首先考虑过通过Gradle自动替换代码生成Jar,但是略显复杂;采用了下方的基于虚拟宏的方式:

  1. 远端代码需要在remote包下编写
  2. remote包不参与插件jar包编译
  3. remote包独立使用java 1.5编译版本,并作为独立资源文件放入/remote/下,并且修改后缀名为.bytecode防止本地加载jar时被误加载
  4. 插件可以动态通过macro工具类完成虚拟宏操作

目前提供虚拟宏如下:

指令格式描述示例
bytecodebytecode className获取remote包的类字节码,注意不需要再传入remote包名bytecode FineJDBCUtils

MacroLoader

首先需要实现的虚拟宏的加载器,加载器从/macro/目录下加载对应的远端字节码,代码如下:

package top.evalexp.higan.plugins.util;  
  
import java.io.ByteArrayOutputStream;  
import java.io.InputStream;  
import java.util.Base64;  
import java.util.HashMap;  
import java.util.Map;  
  
public class MacroLoader {  
    interface MacroProcessor {  
        String processMacro(String args);  
    }  
  
    private static final Map<String, MacroProcessor> macroProcessors = new HashMap<>();  
  
    static {  
        macroProcessors.put("bytecode", args -> {  
            try {  
                ByteArrayOutputStream outputStream;  
                try (InputStream inputStream = MacroLoader.class.getResourceAsStream(String.format("/macro/%s.bytecode", args.replace('.', '/')))) {  
                    outputStream = new ByteArrayOutputStream();  
                    byte[] buffer = new byte[1024];  
                    int length;  
                    if (inputStream != null) {  
                        while ((length = inputStream.read(buffer)) != -1) {  
                            outputStream.write(buffer, 0, length);  
                        }  
                    }  
                }  
                return Base64.getEncoder().encodeToString(outputStream.toByteArray());  
            } catch (Exception e) {  
                return null;  
            }  
        });  
    }  
  
    public static String loadMacro(String macro) {  
        StringBuilder marcoOrder = new StringBuilder();  
        StringBuilder marcoArgs = new StringBuilder();  
        boolean isOrder = true;  
        for (int i = 0; i < macro.length(); i++) {  
            if (macro.charAt(i) == ' ') {  
                isOrder = false;  
                continue;  
            }  
            if (isOrder) {  
                marcoOrder.append(macro.charAt(i));  
            } else {  
                marcoArgs.append(macro.charAt(i));  
            }  
        }  
        return macroProcessors.containsKey(marcoOrder.toString()) ? macroProcessors.get(marcoOrder.toString()).processMacro(marcoArgs.toString()) : null;  
    }  
}

帆软JDBC导出示例

可见帆软JDBC导出#插件代码,其中使用到的语句为

private static final String pluginBytes = MacroLoader.loadMacro("bytecode FineJDBCUtils");

Gradle自处理

想要MacroLoader能正常使用,就必须要配合Gradle编译自处理。

Remote编译

需要添加一个任务:

tasks.register('compileRemote', JavaCompile) {  
    group 'build'  
    source = fileTree('src/main/java') {  
        include 'remote/**'  
    }  
  
    sourceCompatibility = JavaVersion.VERSION_1_5  
    targetCompatibility = JavaVersion.VERSION_1_5  
    def outDir = layout.buildDirectory.dir("classes/main/remote")  
    destinationDirectory = outDir  
  
    classpath = sourceSets.main.compileClasspath  
  
    doLast {  
        file(outDir.get()).eachFileRecurse { file ->  
            if (file.name.endsWith(".class")) {  
                file.renameTo(file.absolutePath.substring(0, file.absolutePath.length() - 6) + ".bytecode")  
            }  
        }  
    }
}
 
 

然后在资源处理中依赖并加入资源文件中:

tasks.processResources {  
    dependsOn 'compileRemote'  
  
    from layout.buildDirectory.dir("classes/main/remote")  
}

插件构建

需要剔除掉remote的编译:

tasks.build {  
    sourceSets {  
        main {  
            java {  
                exclude('remote/**')  
            }  
        }
    }
}

然后指定插件信息:

tasks.jar {  
    manifest {  
        attributes "Plugin-Author": author  
        attributes "Plugin-Version": version  
        attributes "Plugin-Display-Name": project.name  
        attributes "Plugin-Name": "${project.group}.${project.name.replace(' ', '')}"  
    }  
  
    archiveFileName = "${project.group}.${project.name}-${version}.jar"  
}

这一套方案下,可以在同一个项目中开发远程执行的字节码以及插件代码。