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);  // 返回Integeri = 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=1scope.put("a", 1);Bindings anotherScope = engine.createBindings(); // 局部变量,a=2anotherScope.put("a", 2);i = engine.eval("a + 2", scope);System.out.println(i.getClass().getSimpleName() + ":" + i);  // a=1,返回3i = 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=numint 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;}@Overridepublic 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();}@Overridepublic 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,使用ByteCodeInMemoryByteCodeInMemory 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> {@Overridepublic 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实现可配置的逻辑

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

插入排序(概述)

描述 插入排序为将一个数插入到以排序好的数组中 目录 描述 原理 特性 代码 原理 我们以升序为例 先将新数插入到数组的最后一位&#xff0c;记录下新数的值 从新数的位置开始往前遍历&#xff0c;如果前一位大于新数的值 则将当前位置修改为前一位的值 如果前一位小…

爬虫案例:有道翻译python逆向

pip install pip install requestspip install base64pip install pycrytodome tools 浏览器的开发者工具&#xff0c;重点使用断点&#xff0c;和调用堆栈 工具网站&#xff1a;https://curlconverter.com/ 简便请求发送信息 flow 根据网站信息&#xff0c;preview,respon…

php之sql代码审计

1 SQL注入代码审计流程 1.1 反向查找流程 通过可控变量(输入点)回溯危险函数 查找危险函数确定可控变量 传递的过程中触发漏洞 1.2 反向查找流程特点 暴力&#xff1a;全局搜索危险函数 简单&#xff1a;无需过多理解目标网站功能与架构 快速&#xff1a;适用于自动化代码审…

RK3588 opencv maliGPU图像拼接

1 左边图 图像大小:1920*1080 2右边图 图像大小:1920*1080 3拼接好的图像 图像大小&#xff1a;1920 *1080 4代码 #include <iostream> #include <opencv2/opencv.hpp> #include <opencv2/highgui.hpp>//图像融合 #include <opencv2/xfeatures2d.…

基于SpringBoot和Mybatis实现的留言板案例

目录 一、需求及界面展示 二、准备工作 引入依赖 .yml文件相关配置 数据库数据准备 三、编写后端代码 需求分析 代码结构 Model Mapper Service Controller 前端代码 四、测试 一、需求及界面展示 需求&#xff1a; 1. 输入留言信息&#xff0c;点击提交&…

qt-C++笔记之使用QtConcurrent异步地执行槽函数中的内容,使其不阻塞主界面

qt-C笔记之使用QtConcurrent异步地执行槽函数中的内容&#xff0c;使其不阻塞主界面 code review! 文章目录 qt-C笔记之使用QtConcurrent异步地执行槽函数中的内容&#xff0c;使其不阻塞主界面1.QtConcurrent::run基本用法基本用法启动一个全局函数或静态成员函数使用 Lambda…

iOS--锁的学习

iOS--锁的学习 锁的介绍线程安全 锁的分类自旋锁和互斥锁OSSpinLockos_unfair_lockpthread_mutexpthread_mutex的属性 NSLockNSRecursiveLockNSConditionNSConditionLockdispatch_semaphoredispatch_queuesynchronizedatomicpthread_rwlock&#xff1a;读写锁dispatch_barrier_…

摸鱼大数据——Hive基础理论知识——Hive基础架构

1、Hive和MapReduce的关系 1- 用户在Hive上编写数据分析的SQL语句&#xff0c;然后再通过Hive将SQL语句翻译成MapReduce程序代码&#xff0c;最后提交到Yarn集群上进行运行 2- 大家可以将Hive理解成有道词典&#xff0c;帮助你翻译英文 2、Hive架构 用户接口: 包括 CLI、JDBC/…

Java+Swing+Mysql实现飞机订票系统

一、系统介绍 1.开发环境 操作系统&#xff1a;Win10 开发工具 &#xff1a;Eclipse2021 JDK版本&#xff1a;jdk1.8 数据库&#xff1a;Mysql8.0 2.技术选型 JavaSwingMysql 3.功能模块 4.数据库设计 1.用户表&#xff08;users&#xff09; 字段名称 类型 记录内容…

脑机接口习题

9-12章习题 填空题 EEG电极分为 主动电极 和 被动电极 &#xff0c;其中 被动电极 直接与放大器连接&#xff0c; 主动电极 包含一个1~10倍的前置放大。除抗混淆滤波器&#xff0c;放大系统也包含由电阻器、电容器构成的模拟滤波器&#xff0c;把信号频率内容限制在一个特定的…

B树与B+树区别

B树和B树是常见的数据库索引结构&#xff0c;都具有相较于二叉树层级较少&#xff0c;查找效率高的特点&#xff0c;它们之间有以下几个主要区别&#xff1a; 1.节点存储数据的方式不同 B树的叶子结点和非叶子节点都会存储数据&#xff0c;指针和数据共同保存在同一节点中B树…

当标签中出现输入了字母或者数字直接在一行上,没有换行的 情况时怎么办

当标签块中输入的是包含字母或者数字的时候&#xff0c;他不会换行&#xff0c;在一行上显示滚动条的形式&#xff0c;而我们想让他走正常文档流&#xff0c;该换行的时候换行 想要的如下效果 给相应的元素块添加该代码即可 word-break: break-all; .card-content { …

酷开科技大屏营销,多元需求唤醒“客厅经济”

随着科技的发展和消费者习惯的变化&#xff0c;OTT大屏营销正逐渐成为客厅经济的新风向。OTT不仅改变了人们获取信息和娱乐的方式&#xff0c;也为品牌营销提供了新的机遇和挑战&#xff0c;OTT大屏营销已经成为客厅经济的重要组成部分。酷开科技通过其自主研发的智能电视操作系…

一文了解 - GPS/DR组合定位技术

GPS Global Position System 全球定位系统这个大家都很熟悉&#xff0c; 不做太多介绍。 DR Dead Reckoning 车辆推算定位法&#xff0c; 一种常用的辅助的车辆定位技术。 DR系统的优点&#xff1a; 不需要发射和接收信号&#xff1b; 不受电磁波干扰。 DR系统的缺点&#x…

项目管理-质量管理

目录 一、质量管理概述 1.1 GB/T16260.1-2006 定义 1.2 GB/T19000-ISO 9000(2000)系列标准定义 二、软件质量模型 2.1 软件全生命周期质量模型 2.1.1 内部和外部质量的质量模型 2.1.2 使用质量的质量模型 2.1.3 McCall 质量模型 2.1.4 质量特性度量 2.1.5 相关概念 三…

【全开源】多功能投票小程序(ThinkPHP+FastAdmin+Uniapp)

打造高效、便捷的投票体验 一、引言 在数字化快速发展的今天&#xff0c;投票作为一种常见的决策方式&#xff0c;其便捷性和效率性显得尤为重要。为了满足不同场景下的投票需求&#xff0c;我们推出了这款多功能投票小程序系统源码。该系统源码设计灵活、功能丰富&#xff0…

spark实战:实现分区内求最大值,分区间求和以及获取日志文件固定日期的请求路径

spark实战&#xff1a;实现分区内求最大值&#xff0c;分区间求和以及获取日志文件固定日期的请求路径 Apache Spark是一个广泛使用的开源大数据处理框架&#xff0c;以其快速、易用和灵活的特点而受到开发者的青睐。在本文中&#xff0c;我们将通过两个具体的编程任务来展示S…

罗德里格斯公式(旋转矩阵)推导

文章目录 1. 推导2. 性质3. 参考 1. 推导 r r r为旋转轴&#xff0c; θ \theta θ为旋转角度。 先将旋转轴单位化 u r ∣ ∣ r ∣ ∣ u\frac{r}{||r||} u∣∣r∣∣r​ 旋转可以被分为垂直和旋转两个方向&#xff0c; 我们求沿轴方向的分量其实就是在求 p p p向量在 u u u方…

将本地项目上传到 gitee 仓库

1、创建 gitee 仓库 到 gitee 官网&#xff0c;新建仓库 配置新建仓库 完成仓库的创建 项目上传到仓库 上传项目需要安装git git官方下载地址&#xff1a;git下载地址 安装完成&#xff0c;前往本地项目所在文件夹&#xff0c;右击选择 Git Bash Here 刚下载完成需要配置G…