如何构建回测系统(十)

如何构建回测系统(十)

问题

在(九)中实现了一个可以从数据库中编译并加载代码的classloader,但这是有缺陷的

缺陷在于,如果要使用回测系统实现的类和对象则会出现编译无法通过

提示Symbol找不到或者缺失

定位问题

日常我们使用编译功能一般都是maven或者是ant等框架来编译,很少说直接用javac来直接对整个项目编译

在(九)使用的compiler其实就是javac的java代码版本如果采用javac编译一个引用了其他java或者class的文件,则会出现很类似的症状

找到Maven/IDE等工具具体执行javac的代码其实就能看到,是编译时候带上了参数-cp/-classpath/

解决问题

compiler的getTask函数中有个参数叫options,根据文档,这是类似于javac中的参数,但其实如果我们简单的把依赖的class按照-cp的形式添加到参数中就会发现直接报错

这是因为-cp是传给jvm的参数,getTask函数只支持具体的54个参数,属于编译期间具体的信息数据用的

而-cp产生作用的方式是在FileManager中的list()函数起作用的(这里完全没有任何文档,只能单步调试代码发现,所以这里无法说以后就一定正确)

我们只需要将依赖的class/jar/java文件按照需求变更成SimpleJavaFileObject既可

参考代码

因为环境因素,这里的参考代码是在springboot中实现的

package com.michaelssss.domain.strategy;

import static javax.tools.StandardLocation.CLASS_PATH;

import com.michaelssss.exception.RunException;
import com.michaelssss.utils.ArrayUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;
import org.springframework.stereotype.Component;

@Component
public class MyCompiler {

    public byte[] compile(String name, String codes) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        var diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
        var fileManager = new MyJavaFileObjectManager(
            compiler.getStandardFileManager(diagnosticCollector, Locale.CHINESE,
                StandardCharsets.UTF_8));
        fileManager.complieName = name;
        var javaFileObjects = Collections
            .singletonList(
                new MyJavaFileObject(name.replace('.', '/'), codes, fileManager.outputStream));
        var options = new ArrayList<String>();
        var compilerTask = compiler
            .getTask(null, fileManager, diagnosticCollector, options, null, javaFileObjects);
        var result = compilerTask.call();
        if (Objects.nonNull(result) && result) {
            return fileManager.outputStream.toByteArray();
        } else {
            StringBuilder stringBuilder = new StringBuilder();
            diagnosticCollector.getDiagnostics()
                .forEach(diagnostic -> stringBuilder.append(
                    diagnostic.toString()
                ).append("\r\n"));
            throw new RunException(stringBuilder.toString());
        }
    }

    static class MyJavaFileObjectManager extends ForwardingJavaFileManager<JavaFileManager> {

        Map<String, JavaFileObject> fileObjects = new HashMap<>();
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        List<JavaFileObject> result;
        String complieName;

        public MyJavaFileObjectManager(JavaFileManager fileManager) {
            super(fileManager);
        }

        @Override
        public JavaFileObject getJavaFileForInput(Location location, String className,
            Kind kind) throws IOException {
            JavaFileObject javaFileObject = fileObjects.get(className);
            if (!location.getName().contains(CLASS_PATH.getName())) {
                return super.getJavaFileForInput(location, className, kind);
            }
            if (Objects.isNull(javaFileObject)) {
                if (Objects.isNull(result)) {
                    result = CompilerDependenceResolver.resolve();
                }
                var aa = result.stream().filter(
                    f -> f.getName().equalsIgnoreCase(className) && kind.equals(f.getKind()))
                    .collect(
                        Collectors.toList());
                if (aa.isEmpty()) {
                    return super.getJavaFileForInput(location, className, kind);
                } else {
                    return aa.get(0);
                }
            }
            return javaFileObject;
        }

        @Override
        public JavaFileObject getJavaFileForOutput(Location location, String qualifiedClassName,
            JavaFileObject.Kind kind, FileObject sibling) throws IOException {
            JavaFileObject javaFileObject = new MyJavaFileObject(qualifiedClassName, kind,
                outputStream);
            fileObjects.put(qualifiedClassName, javaFileObject);
            return javaFileObject;
        }

        @Override
        public String inferBinaryName(Location location, JavaFileObject file) {
            if (file instanceof MyJavaFileObject) {
                return file.getName();
            }
            if (file instanceof WithPackageAccessSimplaJava) {
                return ((WithPackageAccessSimplaJava) file).getBinaryName();
            }
            if (
                file instanceof NewJarJavaFileObject
            ) {
                return ((NewJarJavaFileObject) file).getBinaryName();
            } else {
                return super.inferBinaryName(location, file);
            }
        }

        @Override
        public Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds,
            boolean recurse) throws IOException {

            if (location.getName().contains(CLASS_PATH.getName())) {
                if (Objects.isNull(result)) {
                    result = CompilerDependenceResolver.resolve();
                }
                if (!result.isEmpty()) {
                    var a = result.stream()
                        .filter(
                            r -> ArrayUtils
                                .isContain(((MySimpleJavaObjectI) r).getBinaryName().split("\\."),
                                    packageName.split("\\.")) && kinds
                                .contains(r.getKind()))
                        .collect(Collectors.toList());
                    var e = super.list(location, packageName, kinds, recurse);
                    e.forEach(a::add);
                    return a;
                }
            }
            return super.list(location, packageName, kinds, recurse);
        }
    }


    static class MyJavaFileObject extends SimpleJavaFileObject {

        String source;
        OutputStream outputStream;
        String name;

        public MyJavaFileObject(String name, Kind kind, OutputStream stream) {
            super(URI.create("String:///" + name + kind.extension), kind);
            this.outputStream = stream;
        }

        public MyJavaFileObject(String name, String source) {
            super(URI.create("String:///" + name + Kind.SOURCE.extension), Kind.SOURCE);
            this.source = source;
            this.name = name;
        }

        public MyJavaFileObject(String name, String source, OutputStream outputStream) {
            this(name, source);
            this.outputStream = outputStream;
        }

        public MyJavaFileObject(URI uri, Kind kind) {
            super(uri, kind);
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
            return source;
        }

        @Override
        public OutputStream openOutputStream() throws IOException {
            return outputStream;
        }
    }
}

package com.michaelssss.domain.strategy;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import lombok.SneakyThrows;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.system.ApplicationHome;

public class CompilerDependenceResolver {

    /**
     * @return 返回所有非SystemLoader加载的class的JavaFileObject
     */
    public static List<String> getClassPath() {
        var systemType = System.getProperty("os.name");

        var classpath = new String[0];
        if (systemType.contains("Windows")) {
            classpath = System.getProperty("java.class.path").split(";");
        }
        if (systemType.contains("Linux")) {
            classpath = System.getProperty("java.class.path").split(":");
        }
        return Arrays.asList(classpath);
    }

    @SneakyThrows
    public static List<JavaFileObject> resolve() {
        List<JavaFileObject> result = new ArrayList<>();
        //查询所有非springboot主文件本身的jar
        final List<String> jarlibs = new ArrayList<>();
        var loadPath = System.getProperty("loader.path");
        var classpath = System.getProperty("java.class.path").split(";");
        if (StringUtils.isNotEmpty(loadPath)) {
            classpath = Files.list(Path.of(loadPath))
                .filter(path -> !Files.isDirectory(path) && path.toString().endsWith(".jar"))
                .map(Path::toString).toArray(String[]::new);
        }
        jarlibs.addAll(Arrays.stream(classpath).filter(s -> s.endsWith(".jar"))
            .collect(Collectors.toList()));
        var compilerModuleClassPaths = Arrays.stream(classpath).filter(s -> !s.endsWith(".jar"))
            .collect(Collectors.toList());

        if (jarlibs.isEmpty()) {
            throw new Error("load jar info failed");
        }
        jarlibs.forEach(s -> {
            try {
                JarFile jarFile = new JarFile(s);
                jarFile
                    .stream()
                    .forEach(jarEntry -> {
                        if (!jarEntry.isDirectory() && jarEntry.getName().endsWith(".class")) {
                            try (var in = jarFile.getInputStream(jarEntry)) {
                                var jarPath = Path.of(s);
                                String separator = jarEntry.getName().startsWith("/") ? "!" : "!/";
                                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                                IOUtils.copy(in, outputStream);
                                result.add(
                                    new NewJarJavaFileObject(
                                        URI.create(
                                            "jar:///" + jarPath.toUri().normalize() + separator
                                                + jarEntry
                                                .getName()),
                                        Kind.CLASS,
                                        outputStream,
                                        jarEntry
                                    ));
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    });

            } catch (IOException e) {
                e.printStackTrace();
            }

        });
        if (!isJar()) {
            var path = Path.of(getPath());
            Files.walkFileTree(path, new FileVisitor<Path>() {
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException {
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                    if (file.toString().endsWith(".class")) {

                        WithPackageAccessSimplaJava withPackageAccessSimplaJava = new WithPackageAccessSimplaJava(
                            path.toUri(),
                            Kind.CLASS, path.normalize().toString(), file);
                        result.add(withPackageAccessSimplaJava);
                    }
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFileFailed(Path file, IOException exc)
                    throws IOException {
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                    throws IOException {
                    return FileVisitResult.CONTINUE;
                }
            });
            compilerModuleClassPaths.forEach(path1 -> {
                try {
                    Files.walkFileTree(Path.of(path1), new FileVisitor<Path>() {
                        @Override
                        public FileVisitResult preVisitDirectory(Path dir,
                            BasicFileAttributes attrs)
                            throws IOException {
                            return FileVisitResult.CONTINUE;
                        }

                        @Override
                        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                            throws IOException {
                            if (file.toString().endsWith(".class")) {
                                WithPackageAccessSimplaJava withPackageAccessSimplaJava = new WithPackageAccessSimplaJava(
                                    Path.of(path1).toUri(),
                                    Kind.CLASS, path1, file);
                                result.add(withPackageAccessSimplaJava);
                            }
                            return FileVisitResult.CONTINUE;
                        }

                        @Override
                        public FileVisitResult visitFileFailed(Path file, IOException exc)
                            throws IOException {
                            return FileVisitResult.CONTINUE;
                        }

                        @Override
                        public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                            throws IOException {
                            return FileVisitResult.CONTINUE;
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        } else {
            var springbootjarpath = Arrays.stream(System.getProperty("java.class.path").split(";"))
                .filter(s -> s.endsWith(".jar"));
            var tempPath = Path.of(getPath()).getParent().resolve("temp");
            if (Files.notExists(tempPath)) {
                Files.createDirectories(tempPath);
            }
            springbootjarpath.forEach(s -> {
                try {
                    //直接解压

                    var jarFile = new JarFile(s);
                    jarFile.stream().filter(jarEntry -> jarEntry.getName().contains("BOOT-INF"))
                        .filter(jarEntry -> !jarEntry.isDirectory())
                        .forEach(jarEntry -> {
                            try {
                                if (Files
                                    .notExists(tempPath.resolve(jarEntry.getName()).getParent())) {
                                    Files.createDirectories(
                                        tempPath.resolve(jarEntry.getName()).getParent());
                                }

                                Files.deleteIfExists(tempPath.resolve(jarEntry.getName()));
                                var filePath = Files
                                    .createFile(tempPath.resolve(jarEntry.getName()));

                                var ou = Files.newOutputStream(filePath);
                                var in = jarFile.getInputStream(jarEntry);
                                IOUtils.copy(in, ou);
                                in.close();
                                ou.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        });

                } catch (IOException e) {
                    e.printStackTrace();
                }
            });

            Files.walk(tempPath.resolve("BOOT-INF"))
                .filter(f -> !Files.isDirectory(f))
                .forEach(s -> {
                    if (s.toString().endsWith(".jar")) {
                        try {
                            JarFile jarFile = new JarFile(s.toString());
                            jarFile
                                .stream()
                                .forEach(jarEntry -> {
                                    if (!jarEntry.isDirectory() && jarEntry.getName()
                                        .endsWith(".class")) {
                                        try (var in = jarFile.getInputStream(jarEntry)) {
                                            var jarPath = Path.of(s.toString());
                                            String separator =
                                                jarEntry.getName().startsWith("/") ? "!" : "!/";
                                            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                                            IOUtils.copy(in, outputStream);
                                            result.add(
                                                new NewJarJavaFileObject(
                                                    URI.create(
                                                        "jar:///" + jarPath.toUri().normalize()
                                                            + separator
                                                            + jarEntry
                                                            .getName()),
                                                    Kind.CLASS,
                                                    outputStream,
                                                    jarEntry
                                                ));
                                        } catch (IOException e) {
                                            e.printStackTrace();
                                        }
                                    }
                                });

                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    } else if (s.toString().endsWith(".class")) {
                        WithPackageAccessSimplaJava withPackageAccessSimplaJava = new WithPackageAccessSimplaJava(
                            s.toUri(),
                            Kind.CLASS,
                            tempPath.resolve("BOOT-INF").resolve("classes").normalize().toString(),
                            s);
                        result.add(withPackageAccessSimplaJava);
                    }
                });
        }
        return result;
    }

    public static String getPath() {
        ApplicationHome home = new ApplicationHome(DBClazz.class);
        return home.getSource().getPath();
    }

    /**
     * 判断是否jar模式运行
     */
    public static boolean isJar() {
        return StringUtils.isNotEmpty(System.getProperty("loader.path"));
    }


    public static void main(String[] args) {
        resolve();
    }
}