Java核心: 脚本引擎和动态编译

静态语言和动态语言的在相互吸收对方的优秀特性,取人之长补己之短。脚本引擎和动态编译就是其中一个关键特性,扩展了Java的能力边界。这一篇我们主要讲两个东西:

  1. ScriptEngine,执行脚本语言代码,如JavaScript、Groovy
  2. JavaCompiler,动态编译Java代码,加载Class对象并使用

1. 脚本引擎

1. 获取引擎

Java通过ScriptEngine接口提供脚本引擎的支持。在ScriptEngineManager里,通过Java SPI加载所有的ScriptEngine的实现。在Java 17中没有自带任何ScriptEngine实现,从侧面反映,其实ScriptEngine并不是一个主流或官方推荐的操作。下面的代码能够获取当前环境下支持的所有脚本引擎。

ScriptEngineManager manager = new ScriptEngineManager();
List<ScriptEngineFactory> factoryList = manager.getEngineFactories();
System.out.println(factoryList.size()); // 默认size = 0,需要自己引用ScriptEngine的实现

factoryList.forEach(x -> {
    System.out.println(x.getEngineName());
    System.out.println(x.getNames());
    System.out.println(x.getMimeTypes());
    System.out.println(x.getExtensions());
});

OpenJDK一个JavaScript的脚本引擎: nashorn,要使用它,需要引入Maven依赖,Maven依赖如下:

<dependency>
    <groupId>org.openjdk.nashorn</groupId>
    <artifactId>nashorn-core</artifactId>
    <version>15.4</version>
</dependency>

引入依赖后,重新执行获取所有可用的脚本引擎代码,我们能看到如下输出,这里主要关心的是ScriptEngine的getNames、getMimeTypes、getExtensions这3个方法的输出

对应这三种输出,我们可以通过ScriptEngineManager中的三个方法,以输出值的一个元素为入参,获取对应的ScriptEngine对象,代码如下

ScriptEngine engine = manager.getEngineByName("nashorn");

engine = manager.getEngineByMimeType("application/javascript");
engine = manager.getEngineByExtension("js");

这里我们提供一个工具方法,用于获取ScriptEngine,供后续测试使用,本文中我们只会使用到nashorn脚本引擎

private static ScriptEngine getEngineByName(String name) {
    ScriptEngineManager manager = new ScriptEngineManager();
    return manager.getEngineByName(name);
}
2. 简单脚本

拿到ScriptEngine对象后,我们就能用它执行脚本了。下面是一个极简的例子,对两个数个数字1和2求和,返回结果3

private static void basicEval() throws ScriptException {
    ScriptEngine engine = getEngineByName("javascript");
    Integer i = (Integer) engine.eval(" 1 + 2");
    System.out.println(i.getClass().getSimpleName() + ":" + i);  // 输出:  Integer: 3
}

这样执行脚本的话,每次都需要我们生成完整的脚本,如果逻辑完全相同,只是其中部分字段值不同,我们能否复用之前的代码呢?ScriptEngine支持在脚本内使用外部定义的变量,有2种方式定义变量:

  1. 引擎变量,同一个ScriptEngine实例下共享变量
  2. 局部变量,同一个Bindings实例下共享变量

我们来看一个引擎变量的例子,调用ScriptEngine.put能设置一个变量的值,并在脚本内使用。这里有一点陷阱是,使用变量后,eval方法返回的类型变成了Double类型。

private static void engineBindEval() throws ScriptException {
    ScriptEngine engine = getEngineByName("javascript");
    engine.put("a", Integer.valueOf(1));
    Object i = engine.eval(" 1 + 2");
    System.out.println(i.getClass().getSimpleName() + ":" + i);  // 返回Integer

    i = engine.eval(" a + 2");
    System.out.println(i.getClass().getSimpleName() + ":" + i);  // 返回Double
}

引擎变量是全局的,更常用的场景是局部变量,Bindings类提供了局部变量的支持,不同Bindings内定义的变量互不干扰

private static void bindEval() throws ScriptException {
    ScriptEngine engine = getEngineByName("javascript");

    Bindings scope = engine.createBindings();        // 局部变量,a=1
    scope.put("a", 1);

    Bindings anotherScope = engine.createBindings(); // 局部变量,a=2
    anotherScope.put("a", 2);

    i = engine.eval("a + 2", scope);
    System.out.println(i.getClass().getSimpleName() + ":" + i);  // a=1,返回3

    i = engine.eval("a + 2", anotherScope);
    System.out.println(i.getClass().getSimpleName() + ":" + i);  // a=2,返回4
}
3. 函数调用

通过在脚本内使用变量,使用不同的变量值,确实支持了脚本的部分逻辑复用。对于复杂的逻辑,可能需要定义多个函数,判断和组合不同的函数来实现。ScriptEngine提供了函数调用的支持,可以事先定义函数,后续反复调用这个函数。来看个实例,我们定义sum函数,实现了两个入参a、b的+操作。

private static void callFunction() throws ScriptException, NoSuchMethodException {
    ScriptEngine engine = getEngineByName("javascript");
    engine.eval("function sum(a,b) {return a + b; }");

    Object r = ((Invocable) engine).invokeFunction("sum", "hello", " world");
    System.out.println(r.getClass().getSimpleName() + ": " + r);

    r = ((Invocable) engine).invokeFunction("sum", 1, 2);
    System.out.println(r.getClass().getSimpleName() + ": " + r);
}
4. 方法调用

很多脚本语言也支持面向对象的编程,比如JavaScript通过prototype能支持对象方法的定义。下面的例子,我们定义了一个Rectangle类,支持方法volume传入一个高(height)配合Rectangle计算体积

private static void callMethod() throws ScriptException, NoSuchMethodException {
    ScriptEngine engine = getEngineByName("javascript");
    engine.eval("function Rectangle(width,length) {this.width=width;this.length=length;}");
    engine.eval("Rectangle.prototype.volume = function(height) {return this.width * this.length * height;}");  // 定义一个Rectangle类

    Object rectangle = engine.eval("new Rectangle(2,3)");                                                      // 创建一个Rectangle实例
    System.out.println(rectangle.getClass().getSimpleName() + ": " + rectangle);
    ScriptObjectMirror mirror = (ScriptObjectMirror) rectangle;
    System.out.println("mirror, width: " + mirror.get("width") + ", length: " + mirror.get("length"));         // 通过ScriptObjectMirror读取字段

    Object volume = ((Invocable) engine).invokeMethod(rectangle, "volume", 4);                                 // 调用实例的方法
    System.out.println(volume.getClass().getSimpleName() + ": " + volume);
}
5. 接口调用

通过方法调用、函数调用,我们已经能复用代码了,但是对于使用者来说还必须使用脚本引擎的API。通过Invocable.getInterface(),我们能拿到一个接口的实例,对使用者来说,只需要关系这个接口即可。Invocable.getInterface()同样也支持两种形式,函数调用和方法调用。下面是一个函数调用的例子,Invocable.getInterface的入参直接是一个Class对象

public static interface NumericFunction {
    public int sum(int a, int b);

    public int multiply(int a, int b);
}

private static void callInterface() throws ScriptException, NoSuchMethodException {
    ScriptEngine engine = getEngineByName("javascript");
    engine.eval("function sum(a,b) { return a + b; }");
    engine.eval("function multiply(a,b) { return a * b; }");

    NumericFunction numeric = ((Invocable) engine).getInterface(NumericFunction.class);  // 获取NumericFunction的实例
    int sum = numeric.sum(1, 2);
    System.out.println("sum: " + sum);
    int product = numeric.multiply(2, 3);
    System.out.println("product: " + product);
}

如果是方法调用,需要在Invocable.getInterface中额外传入一个隐式参数(Java里的this)

public static interface Numeric {
    public int sum(int b);

    public int multiply(int b);
}

private static void callInterfaceMethod() throws ScriptException, NoSuchMethodException {
    ScriptEngine engine = getEngineByName("javascript");
    engine.eval("function Numeric(a) {this.num = a;}");
    engine.eval("Numeric.prototype.sum = function(b) { return this.num + b; }");
    engine.eval("Numeric.prototype.multiply = function(b) { return this.num * b; }");

    Object num = engine.eval("new Numeric(1)");                                    
    Numeric numeric = ((Invocable) engine).getInterface(num, Numeric.class);
   // 方法调用的隐式参数: this=num
    
    int sum = numeric.sum(2);
    System.out.println("sum: " + sum);
    int product = numeric.multiply(3);
    System.out.println("product: " + product);
}
6. 编译脚本

到现在ScriptEngine已经够好用了,我们来看看它的执行效率。对比Java原生代码、脚本引擎、编译脚本执行相同的逻辑,来对比差异。我们先准备测试环境,预定义ScriptEngine、CompiledScript

private static ScriptEngine engine = getEngineByName("nashorn");
private static Bindings params = engine.createBindings();
private static CompiledScript script;

static {
    try {
        script = ((Compilable) engine).compile("a+b");
    } catch (Exception e) {
    }
}
private static volatile Object result;

然后定义一个模板方法(abTest),生成100w个数字,执行BiFunction操作,赋值给result(避免编译器优化),来看看响应时间

private static void abTest(BiFunction<Integer, Integer, Object> func) throws ScriptException {
    Instant now = Instant.now();
    IntStream.range(0, 100_0000).forEach(i -> {
        result = func.apply(i, i + 1);
    });
    Instant finish = Instant.now();
    System.out.println("duration: " + Duration.between(now, finish).toMillis());
}

首先我们使用原生Java来执行求和操作,作为基准测试,代码如下,耗时17ms

abTest((Integer i, Integer j) -> i + j);

然后使用ScriptEngine.eval来执行相同的求和操作,代码如下,耗时2175ms

private static Object eval(Integer a, Integer b) {
    params.put("a", a);
    params.put("b", b);
    try {
        return engine.eval("a+b", params);
    } catch (ScriptException e) {
        throw new RuntimeException(e);
    }
}
// 测试代码
abTest(TestSplit::eval);

然后使用事先编译的脚本CompiledScript做求和操作,代码如下,耗时263ms

private static Object evalCompiled(Integer a, Integer b) {
    params.put("a", a);
    params.put("b", b);
    try {
        return script.eval(params);
    } catch (ScriptException e) {
        throw new RuntimeException(e);
    }
}
// 测试代码
abTest(TestSplit::evalCompiled);

可以看到原生Java的速度要变脚本引擎快将近100倍,使用编译后脚本的速度是为编译版本的10倍。应该说ScriptEngine对性能是有明显的影响的,如无必要尽量不要使用脚本引擎。

2. 在线编译

上一节说到,使用ScriptEngine对性能有负面的影响,而且使用脚本我们需要额外的学习脚本语法。Java提供了JavaCompiler支持动态的编译和执行Java代码,正好能解决上述两个问题。

1. 编译文件

通过JavaCompiler可以编译本地文件,下面是一个简单的示例,值得注意的是,如果使用文件的相对路径,是当前进程的工作目录出发的,通过Path.of("")能拿到进程的工作目录

private static void testCompileFile() throws UnsupportedEncodingException {
    Path path = Path.of("");
    System.out.println(path.toAbsolutePath()); // 打印工作目录,本地文件的相对路径是从工作目录出发的

    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    int result = compiler.run(null, os, os, "src/main/java/com/keyniu/compiler/Demo.java");
    System.out.println("compileResult: " + result + " ,message: " + new String(os.toByteArray(), "utf-8"));
}

如果编译成功,会在.java文件所在目录生成对应的.class文件。JavaCompiler.run返回值0表示编译成功,否则都是编译失败,支持4个入参,分别如下:

参数

说明

InputStream in

JavaCompiler不接受输入,所以这个实参永远是null

OutputStream out

标准输出流,打印编译的过程信息

OutputStream err

错误输出流,打印编译错误信息

String...arguments

运行时参数,使用javac命令时提供的参数

2. 错误信息

调用JavaCompiler.run能通过输出流打印编译的进展和错误信息,然而这些信息适合于人类阅读,并不适合程序分析。CompilationTask增强了JavaCompiler的能力,支持调用者提供DiagnosticListener实例,在编译发生错误时,将错误信息传递给report方法,调用report方法的入参是一个Diagnostic对象,通过Diagnostic对象能取到错误信息、错误代码、错误发生的行和列

private static class MyDiagnostic implements DiagnosticListener {
    public void report(Diagnostic d) {
        System.out.println(d.getKind());
        System.out.println(d.getMessage(Locale.getDefault()));
        System.out.println(d.getCode());
        System.out.println(d.getLineNumber());
        System.out.println(d.getColumnNumber());
    }
}

// 使用CompileTask编译,支持内存文件、错误处理
private static void testCompileTask() {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager fileManager = compiler.getStandardFileManager(new MyDiagnostic(), null, null);
    Iterable<? extends JavaFileObject> sources = fileManager.getJavaFileObjectsFromStrings(List.of("src/main/java/com/keyniu/compiler/Demo.java"));

    JavaCompiler.CompilationTask task = compiler.getTask(null, null, new MyDiagnostic(), List.of(), null, sources);
    Boolean success = task.call();
    System.out.println("compile success: " + success);
}

Java提供了DiagnosticListener的内置实现类DiagnosticCollector,DiagnosticController会收集所有的Diagnostic对象,供后续分析处理

private static void testUseDiagnosticCollector() {
    DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();

    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager fileManager = compiler.getStandardFileManager(collector, null, null);
    Iterable<? extends JavaFileObject> sources = fileManager.getJavaFileObjectsFromStrings(List.of("src/main/java/com/keyniu/compiler/Demo1.java"));

    JavaCompiler.CompilationTask task = compiler.getTask(null, null, collector, List.of(), null, sources);
    Boolean success = task.call();
    System.out.println("compile success: " + success);

    for (Diagnostic<? extends JavaFileObject> d : collector.getDiagnostics()) { // 遍历错误信息
        System.out.println(d);
    }
}
3. 动态代码

如果只能编译本地文件的话,JavaCompiler对应用开发者来价值不大,通过程序调用javac命令能达到类似效果。如果能编译内存中的代码的话,想象空间就大了。通过实现自己的JavaFileObject,我们能让JavaCompiler支持编译存储在内存中(比如String)中的代码。我们来看一个简单的JavaFileObject,覆写getCharContent方法,返回类的代码。

private static class JavaCodeInString extends SimpleJavaFileObject {
    private String code;
    public JavaCodeInString(String name, String code) {
        super(URI.create("string:///" + name.replace('.', '/') + ".java"), Kind.SOURCE);
        this.code = code;
    }
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
        return this.code;
    }
}

剩下来要做的就是在CompilationTask中使用这个JavaFileObject实现。我们将代码放到局部变量code中,创建JavaCodeInString对象,并传递给CompilationTask.run方法的sources参数

private static void testCodeInString() {
    String code = """
            package com.keyniu.compiler;
            public class Demo {
                private static final int VALUE = 10;
                public static void main(String[] args) {
                    System.out.println(VALUE);
                }
                            
            }
            """;
    List<? extends JavaFileObject> sources = List.of(new JavaCodeInString("com.keyniu.compiler.Demo", code));  // 字符串中的代码

    DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager fileManager = compiler.getStandardFileManager(collector, null, null);

    JavaCompiler.CompilationTask task = compiler.getTask(null, null, collector, List.of(), null, sources);
    Boolean success = task.call();
    System.out.println("compile success: " + success);
}
4. 字节码数组

上一节中我们做到了编译保存在String中的代码,而编译结果仍然会写入到.class文件。我们可以将.class文件放入到classpath的路径中,使用时就能通过Class.forName来加载它。其实还有一个选择,将编译后的字节码直接放在内存里,然后使用Class.define获取对应的Class实例。通过定义一个JavaFileObject,覆写openOutputStream方法,能通过这个输出流接受编译后的字节码

public static class ByteCodeInMemory extends SimpleJavaFileObject {
    private ByteArrayOutputStream bos;
    public ByteCodeInMemory(String name) {
        super(URI.create("bytes:///" + name.replace('.', '/') + ".class"), Kind.CLASS);
    }
    public byte[] getCode() {
        return bos.toByteArray();
    }
    @Override
    public OutputStream openOutputStream() throws IOException {
        bos = new ByteArrayOutputStream();
        return bos;
    }
}

通过覆写JavaFileManager的实现,在getJavaFileForOutput时返回ByteCodeInMemory将这个自定义的JavaFileObject实现传递给框架使用

private static List<ByteCodeInMemory> testByteCodeInMemory() {
    String code = """
            package com.keyniu.compiler;
            public class Demo {
                private static final int VALUE = 10;
                public static void main(String[] args) {
                    System.out.println(VALUE);
                }
                            
            }
            """;
    List<? extends JavaFileObject> sources = List.of(new JavaCodeInString("com.keyniu.compiler.Demo", code));

    DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

    List<ByteCodeInMemory> classes = new ArrayList<>();

    StandardJavaFileManager fileManager = compiler.getStandardFileManager(collector, null, null);
    JavaFileManager byteCodeInMemoryFileManager = new ForwardingJavaFileManager<JavaFileManager>(fileManager) {
        public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
            if (kind == JavaFileObject.Kind.CLASS) { // 如果输出的是class,使用ByteCodeInMemory
                ByteCodeInMemory outfile = new ByteCodeInMemory(className);
                classes.add(outfile);
                return outfile;
            } else {
                return super.getJavaFileForOutput(location, className, kind, sibling);
            }
        }
    };

    JavaCompiler.CompilationTask task = compiler.getTask(null, byteCodeInMemoryFileManager, collector, List.of(), null, sources);
    Boolean success = task.call();
    System.out.println("compile success: " + success);

    System.out.println("classCount: " + classes.size());

    return classes;
}
5. 加载Class

使用自定义个JavaFileObject类ByteCodeInMemory,我们拿到了Java编译后的字节码,只需要将它加载成Class对象,接下来就可以正常的使用这个类。直接使用ClassLoader.defineClass就能根据字节码创建Class实例。为了这里动态加载的Class对象后续的更新和回收,这里我们选择一个自定义ClassLoader。下面是一个极简的代码示例

public static class ByteCodeClassLoader extends ClassLoader {
    private List<ByteCodeInMemory> codes;

    public ByteCodeClassLoader(List<ByteCodeInMemory> codes) {
        this.codes = codes;
    }

    public Class<?> findClass(String name) throws ClassNotFoundException {
        for (ByteCodeInMemory cl : codes) {
            if (cl.getName().equals("/" + name.replace('.', '/') + ".class")) {
                byte[] bs = cl.getCode();
                return defineClass(name, bs, 0, bs.length);
            }
        }
        throw new ClassNotFoundException(name);
    }
}

接着要做的就是使用ClassLoader加载Class对象,后续就可以像正常的Class对象一样使用了。

private static void testClassLoader(List<ByteCodeInMemory> bytecodes) throws ClassNotFoundException {
    ByteCodeClassLoader classLoader = new ByteCodeClassLoader(bytecodes);
    Class<?> clazz = classLoader.findClass("com.keyniu.compiler.Demo");
    Field[] fs = clazz.getDeclaredFields();
    for (Field f : fs) {
        System.out.println(f.getName() + ":" + f.getType());
    }
}
6. 性能测试

现在我们使用JavaCompiler来创建一个第1.6做的性能测试的案例,生成一个BiFunction<Integer,Integer,Object>的实现类

public static BiFunction<Integer, Integer, Object> getSumFunction() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    List<ByteCodeInMemory> bytecodes = compileStringCode();
    ByteCodeClassLoader classLoader = new ByteCodeClassLoader(bytecodes);
    Class<?> clazz = classLoader.findClass("com.keyniu.stream.test.MyNativeSum");
    BiFunction<Integer, Integer, Object> sum = (BiFunction<Integer, Integer, Object>) clazz.newInstance();
    return sum;
}


private static List<ByteCodeInMemory> compileStringCode() {
    String code = """
            package com.keyniu.stream.test;
   
            import java.util.function.BiFunction;

            public class MyNativeSum implements BiFunction<Integer, Integer, Object> {
                @Override
                public Object apply(Integer i1, Integer i2) {
                    return i1 + i2;
                }
            }
            """;
    List<? extends JavaFileObject> sources = List.of(new JavaCodeInString("com.keyniu.stream.test.MyNativeSum", code));

    DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

    List<ByteCodeInMemory> classes = new ArrayList<>();

    StandardJavaFileManager fileManager = compiler.getStandardFileManager(collector, null, null);
    JavaFileManager byteCodeInMemoryFileManager = new ForwardingJavaFileManager<JavaFileManager>(fileManager) {
        public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
            if (kind == JavaFileObject.Kind.CLASS) {
                ByteCodeInMemory outfile = new ByteCodeInMemory(className);
                classes.add(outfile);
                return outfile;
            } else {
                return super.getJavaFileForOutput(location, className, kind, sibling);
            }
        }
    };

    JavaCompiler.CompilationTask task = compiler.getTask(null, byteCodeInMemoryFileManager, collector, List.of(), null, sources);
    Boolean success = task.call();

    return classes;
}

使用getSumFunction返回的BiFunction<Integer,Integer,Object>的时间耗时22ms,基本接近Java原生代码实现

A. 参考资料

1. Java实现可配置的逻辑

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/646143.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

P459 包装类Wrapper

包装类的分类 1&#xff09;针对八种基本数据类型相应的引用类型——包装类。 2&#xff09;有了类的特点&#xff0c;就可以调用类中的方法。 Boolean包装类 Character包装类 其余六种Number类型的包装类 包装类和基本数据类型的相互转换 public class Integer01 {publi…

关于数据库和数据表的基础SQL

目录 一. 数据库的基础SQL 1. 创建数据库 2. 查看当前有哪些数据库 3. 选中数据库 4. 删除数据库 5. 小结 二. 数据表的基础SQL 1. 创建数据表 2. 查看当前数据库中有哪些表 3. 查看指定表的详细情况(查看表的结构) 4. 删除表 5. 小结 一. 数据库的基础SQL 1. 创建…

Redis篇 redis基本命令和定时器原理

基本命令和定时器原理 一. exists命令二. del命令三. Expire命令四. ttl命令五. redis的过期策略六. 定时器的两种设计方式七. type命令 一. exists命令 用来判断key的值是否存在 返回值是key的个数 这样写的话&#xff0c;有没有什么区别呢&#xff1f; 效率变低&#xff0c;消…

用例篇

弱网测试 弱网测试的目的是尽可能保证用户体验&#xff0c;关注的关键点包括&#xff1a; 页面响应时间是否可以接受&#xff0c;关注包括哦热启动、冷启动时间、页面切换、前后台切换、首字时间&#xff0c;首屏时间等。页面呈现是否完成一致。超时文案是否符合定义&#xf…

YoloV1模型

You Only Look Once 文章目录 You Only Look Once置信度定义类别条件概率NMSnp.maxmiumnp.argsort() Yolov1直接采用网络特征输出&#xff0c;实现置信度预测、分类、边界框回归&#xff1b; 核心内容总结&#xff1a; 输入图像划分为 S S 网格。如果对象的中心落入网格单元中…

Java——认识Java

一、介绍 1、起源 Java 是由 Sun Microsystems 于 1995 年推出的一种面向对象的编程语言和计算平台。由詹姆斯高斯林&#xff08;James Gosling&#xff0c;后来被称为Java之父&#xff09;和他的同事们共同研发。后来&#xff0c;Sun 公司被 Oracle&#xff08;甲骨文&#…

vue3 vite动态根据字符串加载组件

1 原理 import.meta.glob() 其实不仅能接收一个字符串&#xff0c;还可以接收一个字符串数组&#xff0c;就是匹配多个位置 let RouterModules import.meta.glob(["/src/view/*/*.vue", "/src/view/*.vue"]);这样我们就拿到了相对路劲的组件对象&#xf…

leedcode【142】. 环形链表 II——Java解法

Problem: 142. 环形链表 II 思路解题方法复杂度Code性能 思路 1.用快慢指针找到相遇的点&#xff08;快指针一次走一步&#xff0c;慢指针一次走两步&#xff09; 2.一个指针从head开始&#xff0c;一个指针从相遇点开始&#xff0c;一次一步&#xff0c;相遇处即为环入口 解题…

【模版方法设计模式】

文章目录 模板方法设计模式模板方法的设计原则模板方法设计模式组成部分代码实现抽象类实现具体实现类执行 模板方法设计模式 模版方法设计模式&#xff08;Template Method Pattern&#xff09;是一种行为设计模式&#xff0c;它定义了一个操作中的算法骨架&#xff0c;而将一…

网络应用层之(1)DHCPv6协议

网络应用层之(1)DHCPv6协议 Author: Once Day Date: 2024年5月26日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文章可参考专栏: 通信网络技术_Once-Day的博客-C…

设计模式八股文

什么是设计模式&#xff1f; 设计模式是软件开发过程中经常遇到的问题的通用解决方案。类似于前人总结的经验&#xff0c;遇到相似问题的时候有个参考。 设计模式七大基本原则&#xff1f; 单一职责&#xff1a;一个类应该只作一件事情。将功能分为小的独立的单元。开放封闭…

文心一言 VS 讯飞星火 VS chatgpt (267)-- 算法导论20.2 2题

二、写出 PROTO-vEB-DELETE 的伪代码。通过扫描簇内的相关位&#xff0c;来更新相应的 summary 位。并且你实现的伪代码的最坏情况运行时间是多少&#xff1f;如果要写代码&#xff0c;请用go语言。 文心一言&#xff1a; 首先&#xff0c;让我们回顾一下vEB&#xff08;Van …

matplotlib latex表格

使用python3环境 import matplotlib.gridspec as gridspec import matplotlib.pyplot as pltimport numpy as np import matplotlib as mpl #mpl.use(pgf)def figsize(scale, nplots 1):fig_width_pt 390.0 # Get this from LaTeX using \the\text…

学 Python 具体能干什么?

Python 是一种功能强大、用途广泛的编程语言&#xff0c;因其简洁易读的语法和丰富的库生态系统而备受欢迎。学习 Python后&#xff0c;你可以从事以下几方面的工作&#xff1a; 1. Web 开发 Python 有很多流行的 Web 框架&#xff0c;如&#xff1a; Django&#xff1a;一个…

无畏并发: Rust Mutex的基本使用

并发是很多编程语言避不开的一块主要内容&#xff0c;主打一个无畏并发的Rust自然也面临这样的挑战。Rust中的Mutex提供了强大的同步原语&#xff0c;确保共享数据的线程安全&#xff0c;这篇文章中&#xff0c;我们会探讨Mutex的使用&#xff0c;从基础的用法到一些高阶内容。…

数据结构(六)图

2024年5月26日一稿(王道P220) 6.1 图的基本概念 6.1.1 图的定义 6.2 图的存储及基本操作 6.2.1邻接矩阵法 6.2.2 邻接表

自动驾驶路径决策算法——动态规划

文章内容来自b站up主忠厚老实的老王&#xff0c;视频链接如下&#xff1a; 自动驾驶决策规划算法第二章第二节(中) 参考线算法_哔哩哔哩_bilibili 其中host是自车位置&#xff0c;以host在参考线的投影为坐标原点&#xff0c;建立frenet坐标&#xff0c;此时host的坐标是(0,L0…

ABAQUS应用07-实现拉伸和压缩刚度不同的弹簧建模

文章目录 0、背景描述1、步骤 0、背景描述 到目前为止&#xff0c;本文的内容我还没有具体实践过&#xff0c;但是个人认为后期是会用到的。比如说&#xff0c;对于风电机组地基转动刚度的设置&#xff0c;土体就是一种拉压刚度并不相同的材料。所以现在先记录下来&#xff0c…

bclinux基于欧拉(BigCloud Enterprise Linux For Euler)下安装mysql5.7

第一步&#xff1a;下载mysql5.7的rpm安装包 下载地址&#xff1a;https://dev.mysql.com/downloads/mysql/ 第二步&#xff1a;上传mysql安装包到Centos7的下 第三步&#xff1a;检查是否已经安装了mysql或者mariadb&#xff08;centos7默认安装&#xff09;&#xff0c;如已…

Java—内部类

Java—内部类 一、内部类二、应用特点三、分类3.1、普通内部类&#xff1a;直接将一个类的定义放在另外一个类的类体中3.2、静态内部类3.3、局部内部类 一、内部类 一个类的定义出现在另外一个类&#xff0c;那么这个出现的类就叫内部类(Inner)。 内部类所在的类叫做外部类(Ou…