基于语法树的SQL自动改写工具开发系列(3)-使用JAVA进行简单SQL改写的开发实战

前言

基于语法树的SQL自动改写工具开发系列(1)-离线安装语法树解析工具antlr4
基于语法树的SQL自动改写工具开发系列(2)-使用PYTHON进行简单SQL改写的开发实战

前两篇分别介绍了如何搭建开发环境,以及如何使用python作为开发语言进行开发。
python做做小活问题不大,但是对于生成语法树这种任务,计算量还是有点大,所以其性能表现非常差。
实际测试中,python版本的解析器,输入一个2万行的create package语句,需要20分钟才能把语法树生成,但使用java版本的解析器,则只要不到1分钟,而且得益于java的缓存机制,这个处理会越来越快,实际平均大概为二十秒左右。
本篇介绍如何使用java作为开发语言基于antlr4进行SQL自动改写的开发

实战

demo1

还是以上一篇中的table函数转换成unnest为例,以下给出一个完整的demo

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;
import org.antlr.v4.runtime.misc.Interval;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;public class PlSqlRewriter {public static void main(String[] args) throws Exception {// 输入文件名String inputFileName = "input.sql";// 输出文件名String outputFileName = "output.sql";// 从文件中读取 PL/SQL 代码String input = new String(Files.readAllBytes(Paths.get(inputFileName)));// 创建词法分析器和解析器PlSqlLexer lexer = new PlSqlLexer(CharStreams.fromString(input));CommonTokenStream tokens = new CommonTokenStream(lexer);PlSqlParser parser = new PlSqlParser(tokens);// 获取语法树ParseTree tree = parser.sql_script();// 创建 TokenStreamRewriterTokenStreamRewriter rewriter = new TokenStreamRewriter(tokens);// 创建自定义的 VisitorPlSqlParserBaseVisitor<Void> visitor = new PlSqlParserBaseVisitor<Void>() {@Override public Void visitTable_collection_expression(PlSqlParser.Table_collection_expressionContext ctx){if (ctx.getText().toLowerCase().startsWith("table")) {String argument = ctx.expression().getText();String newText = "(select * from unnest(" + argument + ") column_value)";rewriter.replace(ctx.start.getTokenIndex(), ctx.stop.getTokenIndex(), newText);}return null;}};// 将改写后的代码输出到文件String output = rewriter.getText();Files.write(Paths.get(outputFileName), output.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);System.out.println("转换后的结果已输出到文件:" + outputFileName);}
}

其实可以发现,用java或者用python来开发antlr4的程序,代码逻辑是差不多的。
如果想要修改某个节点,就去PlSqlParserBaseVisitor.java里找对应的visit函数复制过来,在里面填充自己想要的处理逻辑即可。

需要注意的是,由于这一段我是基于这个节点进行了整体替换,替换后,这个节点内部的语法树就不能用了,因此这里执行完后我写的是return null; ,避免再继续下钻到子节点。大部分的情况下,应该要使用return visitChildren(ctx);,避免改了外层没改内层。

demo2

openGauss不支持 {a} [not] member of {b}语法,需要改成 [not] {a}=any({b})
这里我发现个antlr4的plsql语法bug,它没有支持这个语法中的not,于是提交了代码进行修复
https://github.com/antlr/grammars-v4/pull/4347/files
其实很简单,就只是把

: relational_expression (multiset_type = (MEMBER | SUBMULTISET) OF? concatenation)?

改成了

: relational_expression (multiset_type = NOT? (MEMBER | SUBMULTISET) OF? concatenation)?

这里NOT后面的?表示是此处的语法可以有NOT,也可以没有NOT
语法解析规则修复后,需要重新生成对应的语法解析器

SET CLASSPATH=.:antlr-4.13.2-complete.jar
java org.antlr.v4.Tool -Dlanguage=Java -no-listener -visitor PlSqlLexer.g4 
java org.antlr.v4.Tool -Dlanguage=Java -no-listener -visitor PlSqlParser.g4 

然后根据生成的语法树,找到合适的节点,编写对应的visit函数即可

set CLASSPATH=.:antlr-4.13.2-complete.jar
java org.antlr.v4.gui.TestRig PlSql sql_script -tree declare
L_TYT_A TYT_a:=TYT_a();
L_TYT_VARCHAR2 TYT_VARCHAR2:=TYT_VARCHAR2();
begin
if L_TYT_A(1).B not member of L_TYT_VARCHAR2 then p1;
end if;
end;
/
^z
(sql_script(unit_statement(anonymous_blockdeclare(seq_of_declare_specs(declare_spec(variable_declaration(identifier(id_expression(regular_id L_TYT_A)))(type_spec(type_name(id_expression(regular_id TYT_a))))(default_value_part :=(expression(logical_expression(unary_logical_expression(multiset_expression(relational_expression(compound_expression(concatenation(model_expression(unary_expression(atom(general_element(general_element_part(id_expression(regular_id TYT_a)))(function_argument ())))))))))))));))(declare_spec(variable_declaration(identifier(id_expression(regular_id L_TYT_VARCHAR2)))(type_spec(type_name(id_expression(regular_id TYT_VARCHAR2))))(default_value_part :=(expression(logical_expression(unary_logical_expression(multiset_expression(relational_expression(compound_expression(concatenation(model_expression(unary_expression(atom(general_element(general_element_part(id_expression(regular_id TYT_VARCHAR2)))(function_argument ())))))))))))));)))begin(seq_of_statements(statement(if_statementif(condition(expression(logical_expression(unary_logical_expression(multiset_expression(relational_expression(compound_expression(concatenation(model_expression(unary_expression(atom(general_element(general_element(general_element_part(id_expression(regular_id L_TYT_A)))(function_argument((argument(expression(logical_expression(unary_logical_expression(multiset_expression(relational_expression(compound_expression(concatenation(model_expression(unary_expression(atom(constant(numeric 1)))))))))))))))) . (general_element_part(id_expression(regular_id B)))))))))) not member of (concatenation(model_expression(unary_expression(atom(general_element(general_element_part(id_expression(regular_id L_TYT_VARCHAR2))))))))))))) then(seq_of_statements(statement(call_statement(routine_name(identifier(id_expression(regular_id p1))))));)end if);)end));) / <EOF>)
// 将member of 改成 =any@Overridepublic Void visitMultiset_expression(PlSqlParser.Multiset_expressionContext ctx) {String newExpr;if (ctx.MEMBER() != null && ctx.OF() != null) {// 获取左侧和右侧表达式String leftExpr = ctx.getChild(0).getText();String rightExpr = ctx.getChild(ctx.getChildCount() - 1).getText();// 构建新的表达式if (ctx.NOT() != null ){newExpr = "not (" + leftExpr + " = any(" + rightExpr + "))";} else {newExpr = " (" + leftExpr + " = any(" + rightExpr + "))";}// 替换原有表达式rewriter.replace(ctx.getStart(), ctx.getStop(), newExpr);}return visitChildren(ctx);}

demo3

openGauss不支持存储过程参数中的nocopy,绝大部分场景下可以移除(注意这并非绝对等价,nocopy在异常抛出时,是可能导致程序运行结果不一致的,因为使用指针直接修改了出参的值,而不是执行完后拷贝给出参)

语法树:

create procedure t(a in out  nocopy number,
b in number) is
begin
null;
end;
/
^z
(sql_script(unit_statement(create_procedure_bodycreate procedure(procedure_name(identifier(id_expression(regular_id t))))((parameter(parameter_name(identifier(id_expression(regular_id a))))in out nocopy(type_spec(datatype(native_datatype_element number)))),(parameter(parameter_name(identifier(id_expression(regular_id b))))in(type_spec(datatype(native_datatype_element number)))))is(bodybegin(seq_of_statements(statement(null_statement null));)end))); / <EOF>)

处理代码:

 //移除参数中的nocopy@Overridepublic Void visitParameter(PlSqlParser.ParameterContext ctx) {if (ctx.NOCOPY() != null) {// 删除 nocopyfor (TerminalNode nocopyNode : ctx.NOCOPY()) {rewriter.delete(nocopyNode.getSymbol());}}return visitChildren(ctx);}

这个demo示范了如何在指定节点里删除其中的部分元素

demo4

在ORACLE中,having 子句可以写在group by子句的前面或者后面,而openGauss只支持having子句在group by子句的后面,因此要编写代码,把having子句移动到group by子句后面去

 //将group by 前面的having 移动到group by后@Overridepublic Void visitGroup_by_clause(PlSqlParser.Group_by_clauseContext ctx) {PlSqlParser.Having_clauseContext havingClause = null;Integer groupByFound =0;// 查找 having_clausefor (ParseTree child : ctx.children) {if (child instanceof PlSqlParser.Group_by_elementsContext){groupByFound=1;}if (child instanceof PlSqlParser.Having_clauseContext && groupByFound!=1) {havingClause = (PlSqlParser.Having_clauseContext) child;break;}}// 如果存在 having_clause 且存在 group_by_elementsif (havingClause != null && ctx.group_by_elements() != null && !ctx.group_by_elements().isEmpty()) {// 删除原有的 having_clauserewriter.delete(havingClause.getStart(), havingClause.getStop());// 获取原始的having_clause文本TokenStream tokens = rewriter.getTokenStream();String havingText = tokens.getText(havingClause.getSourceInterval());rewriter.insertAfter(ctx.group_by_elements(ctx.group_by_elements().size() - 1).getStop(), " " + havingText);}return visitChildren(ctx);}

这个demo示范了如何获取节点的原始文本

demo5

ORACLE存储过程的一个声明区域里,支持声明相同名称的变量,在这些变量没有被使用到时,存储过程可以正常编译成功;如果变量有被使用到,则会编译失败。在openGauss中则不允许在同一个声明区域声明相同名称的变量,为了保证代码最少改动,我们可以对重复名称的变量保留最后一个

  //移除重复声明的变量 存储过程自己的变量声明@Overridepublic Void visitSeq_of_declare_specs(PlSqlParser.Seq_of_declare_specsContext ctx) {Map<String, PlSqlParser.Declare_specContext> lastOccurrence = new HashMap<>();List<PlSqlParser.Declare_specContext> declareSpecs = ctx.declare_spec();// 记录每个变量名的最后一个声明for (PlSqlParser.Declare_specContext declareSpec : declareSpecs) {if (declareSpec.variable_declaration() != null) {String varName = declareSpec.variable_declaration().identifier().id_expression().regular_id().getText();lastOccurrence.put(varName, declareSpec);}}// 删除所有重复的声明,保留最后一个for (PlSqlParser.Declare_specContext declareSpec : declareSpecs) {if (declareSpec.variable_declaration() != null) {String varName = declareSpec.variable_declaration().identifier().id_expression().regular_id().getText();if (lastOccurrence.get(varName) != declareSpec) {rewriter.delete(declareSpec.getStart(), declareSpec.getStop());}}}return visitChildren(ctx);}//移除重复声明的变量 存储过程内部匿名块的变量声明@Overridepublic Void visitBlock(PlSqlParser.BlockContext ctx) {Map<String, PlSqlParser.Declare_specContext> lastOccurrence = new HashMap<>();List<PlSqlParser.Declare_specContext> declareSpecs = ctx.declare_spec();// 记录每个变量名的最后一个声明for (PlSqlParser.Declare_specContext declareSpec : declareSpecs) {if (declareSpec.variable_declaration() != null) {String varName = declareSpec.variable_declaration().identifier().id_expression().regular_id().getText();lastOccurrence.put(varName, declareSpec);}}// 删除所有重复的声明,保留最后一个for (PlSqlParser.Declare_specContext declareSpec : declareSpecs) {if (declareSpec.variable_declaration() != null) {String varName = declareSpec.variable_declaration().identifier().id_expression().regular_id().getText();if (lastOccurrence.get(varName) != declareSpec) {rewriter.delete(declareSpec.getStart(), declareSpec.getStop());}}}return visitChildren(ctx);}

这个demo描述了子节点为List时的一种处理方式。

这里注意,我写了两段visit,是因为declare_spec这个节点存在于多个语法分支下,在PlSqlParser.g4里搜索 declare_spec,可以发现有四处,所以这四处理论上都需要进行处理,但另外两处是触发器里的,而openGauss目前仍然不支持ORACLE语法的触发器,所以这两处也就懒得改了。

demo6

ORACLE和openGauss在多行注释上,是存在差异的,ORACLE的多行注释符合C语言的风格,不支持嵌套的多行注释,而openGauss则遵循PG的风格,支持多行的嵌套注释。所以对于ORACLE是正确的注释/*/* comment*/,在openGauss中就不正确了,因为PG风格的多行注释要求有几个 /*,就要有几个*/,就像括号一样,必须成对出现,或者去掉不成对的 /,比如改成/** comment*/

上面几个demo都是按visit的方式进行的处理,这要求需要处理的内容都在语法树中,但是SQL中有些东西是不在可见的语法树中的,比如多行注释,因为注释只在词法规则PlSqlLexer.g4中定义了

MULTI_LINE_COMMENT  : '/*' .*? '*/'                    -> channel(HIDDEN);

这里可以看到后面有个 -> channel(HIDDEN),意思就是遇到前面这个格式的字符串都放到隐藏通道里,不做语法树解析。

这里的MULTI_LINE_COMMENT,是可以在使用工具解析语法树时,加上-tokens打印出来的。

set CLASSPATH=.:antlr-4.13.2-complete.jar
java org.antlr.v4.gui.TestRig PlSql sql_script -tree -tokensselect 1 /*/*/ from dual; 
^Z
[@0,0:5='select',<'SELECT'>,1:0]
[@1,6:6=' ',<SPACES>,channel=1,1:6]
[@2,7:7='1',<UNSIGNED_INTEGER>,1:7]
[@3,8:8=' ',<SPACES>,channel=1,1:8]
[@4,9:13='/*/*/',<MULTI_LINE_COMMENT>,channel=1,1:9]
[@5,14:14=' ',<SPACES>,channel=1,1:14]
[@6,15:18='from',<'FROM'>,1:15]
[@7,19:19=' ',<SPACES>,channel=1,1:19]
[@8,20:23='dual',<REGULAR_ID>,1:20]
[@9,24:24=';',<';'>,1:24]
[@10,25:26='\r\n',<SPACES>,channel=1,1:25]
[@11,27:26='<EOF>',<EOF>,2:0]
(sql_script(unit_statement(data_manipulation_language_statements(select_statement(select_only_statement(subquery(subquery_basic_elements(query_blockselect(selected_list(select_list_elements(expression(logical_expression(unary_logical_expression(multiset_expression(relational_expression(compound_expression(concatenation(model_expression(unary_expression(atom(constant(numeric 1)))))))))))))))(from_clausefrom(table_ref_list(table_ref(table_ref_aux(table_ref_aux_internal(dml_table_expression_clause(tableview_name(identifier(id_expression(regular_id dual))))))))))))))); <EOF>)

由于不能在语法树中处理了,因此这个改动需要在上面我们定义的visitor外面来处理

 public static String removeComment(String input) {if (input.length() <= 4) {return input;}String prefix = input.substring(0, 1);String suffix = input.substring(input.length() - 1);String middle = input.substring(1, input.length() - 1);middle = middle.replaceAll("/\\*", "\\*");return prefix + middle + suffix;}
 visitor.visit(tree);Token previousToken = null;Token prePreviousToken = null;//遍历所有tokenfor (Token token : tokens.getTokens()) {// 处理多行注释if (token.getType() == PlSqlLexer.MULTI_LINE_COMMENT) {String comment = token.getText();// 去除注释内部的`/`String modifiedComment = removeComment(comment);rewriter.replace(token, modifiedComment);}}

这个demo演示了如何针对token来进行处理

demo7

ORACLE在使用ZHS16GBK字符集时,能够解析SQL中的全角符号,比如全角逗号、全角括号、全角空格等,但openGauss明显是不支持的,而且目前antlr4的PLSQL词法和语法解析规则也不支持识别这种全角符号。因此,如果要解决这个问题,我们就得先从词法和语法规则上动手了。
先来最简单的,全角空格\u3000
在PlSqlLexer.g4中,找到空格的定义SPACES:

SPACES: ([ \t\r\n]+ )-> channel(HIDDEN);

这里是把连续多个的空格、tab、回车、换行,都视为空格,放入隐藏通道,我们可以把全角空格也加进来,比如

SPACES: ([ \t\r\n]+ |'\u3000')-> channel(HIDDEN);

这里的|表示or ,为了后面方便处理,我把全角空格按单个字符拆开了。
注意,在g4文件中,对于非ascii字符,需要用unicode码表示。

改完g4文件后,要重新生成解析器

SET CLASSPATH=.:antlr-4.13.2-complete.jar
java org.antlr.v4.Tool -Dlanguage=Java -no-listener -visitor PlSqlLexer.g4 
java org.antlr.v4.Tool -Dlanguage=Java -no-listener -visitor PlSqlParser.g4 

然后在上面demo6的遍历所有token的循环里,加上一段

 // 处理全角空格if (token.getType() == PlSqlLexer.SPACES && token.getText().equals("\u3000")) {rewriter.replace(token, ' ');}

就这么简单。

接下来看全角逗号\uFF0C

首先逗号的确是有在词法中定义的

COMMA           : ',';

但是语法规则中,极少用COMMA来表示逗号,绝大多是情况下,是直接用的','这样一个字符串,因此我们需要做两个事情
1.在词法规则中新增一个用来表示全角逗号的规则,
2.在语法规则中,对于所有的 ',',改成 (','或 全角逗号)

PlSqlLexer.g4

PERCENT         : '%';
AMPERSAND       : '&';
LEFT_PAREN      : '(';
RIGHT_PAREN     : ')';
DOUBLE_ASTERISK : '**';
ASTERISK        : '*';
PLUS_SIGN       : '+';
MINUS_SIGN      : '-';
COMMA           : ',';
SOLIDUS         : '/';
AT_SIGN         : '@';
ASSIGN_OP       : ':=';
HASH_OP         : '#';
WIDTH_COMMA : '\uFF0C';

PlSqlParser.g4

selected_list: '*'| select_list_elements ((','|WIDTH_COMMA) select_list_elements)*;

上面这里表示在select的列表里,分隔符可以用半角逗号或者全角逗号,其他地方的逗号也是这样修改,大概有三十几处。
改完g4文件后,重新生成解析器,然后在demo6中遍历token的循环里,加上这么一段

// 处理全角逗号if (token.getType() == PlSqlLexer.WIDTH_COMMA) {rewriter.replace(token, ',');}

总结

相比python而言,java处理语法树的处理性能好太多了,原本用python写几个转换感觉只能当成玩具耍耍,用到java后,感觉这种自动化改写完全可以当成正式的产品来用了。基于语法树改写的比正则替换的风险小多了,而且新增功能也很简单。目前我已经写了几十种ORACLE迁移到openGauss的自动转换规则,并且做了很多自定义的参数配置,后续看情况可能会开源。

PLSQLREWRITE4OPENGAUSS
│  .gitignore
|  batch_rewrite.bat # WINDOWS环境下批量转换脚本
|  batch_rewrite.sh # LINUX环境下批量转换脚本
│  build.bat  #  WINDOWS环境下的构建脚本
│  build.sh   #  LINUX环境下的构建脚本
|  grun_tree.bat  # WINDOWS环境下查看语法树
|  grun_tree.sh  # LINUX环境下查看语法树
│  LICENSE
│  readme.md  # 说明文件
│  regen_parser.bat  # WINDOWS环境下的词法语法解析器重新生成脚本
│  regen_parser.sh   # LINUX环境下的词法语法解析器重新生成脚本
│
├─build 构建临时目录
|
├─config 配置文件目录
│    atom_mapping.properties # 使用指定函数转换指定的字段名
│    config.properties # 杂项配置
│    datatype_mapping.properties # 数据类型映射
|    exception_mapping.properties  # 异常名称映射
|    general_element_mapping.properties  # 函数表达式映射
|    patterns.properties # 正则替换规则
|    query_block_mapping.properties # 查询块规则
|    regular_id_mapping.properties # 字段名、变量名映射
|    token_mapping.properties # token映射
|
├─lib  # 三方库目录
│      antlr-4.13.2-complete.jar # https://www.antlr.org/download/antlr-4.13.2-complete.jar
│
├─src  # 源码目录
│  └─main
│      └─java
│              PlSqlLexer.g4  # 词法解析规则
│              PlSqlParser.g4  # 语法解析规则
│              PlSqlLexerBase.java  # 词法基类
│              PlSqlParserBase.java  # 语法基类
│              PlSqlRewriter.java  # 主程序源码
│
└─test  # 测试目录│ compare.bat  # WINDOWS下比较测试结果是否符合预期│ compare.sh   # LINUX下比较测试结果是否符合预期    │├─except  # 预期测试输出文件│      test.sql│    ├─input   # 测试输入文件│      test.sql│└─output  # 测试输出文件test.sql
  • 本文作者: DarkAthena
  • 本文链接: https://www.darkathena.top/archives/antlr4-part3-dev-with-java
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处

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

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

相关文章

TCP 为什么采用三次握手和四次挥手以及 TCP 和 UDP 的区别

1. TCP 为什么采用三次握手和四次挥手 采用三次握手的原因&#xff1a; 确认双方的收发能力。第一次握手&#xff0c;客户端发送 SYN 报文&#xff0c;告诉服务器自身具备发送数据的能力&#xff0c;第二次握手&#xff0c;服务器回应 SYN ACK 报文&#xff0c;表名自己既能…

python-Flask:SQLite数据库路径不正确但是成功访问到了数据库,并对表进行了操作

出现了这个问题&#xff0c;就好像是我要去找在南方的人&#xff0c;然后我刚好不分南北&#xff0c;我认为的方向错了&#xff0c;实则方向对了。 在我针对复盘解决&#xff1a;sqlite3.OperationalError: unrecognized token: “{“-CSDN博客这个内容的时候&#xff0c;又出现…

2024-12-29-sklearn学习(25)无监督学习-神经网络模型(无监督) 烟笼寒水月笼沙,夜泊秦淮近酒家。

文章目录 sklearn学习(25) 无监督学习-神经网络模型&#xff08;无监督&#xff09;25.1 限制波尔兹曼机25.1.1 图形模型和参数化25.1.2 伯努利限制玻尔兹曼机25.1.3 随机最大似然学习 sklearn学习(25) 无监督学习-神经网络模型&#xff08;无监督&#xff09; 文章参考网站&a…

Spring ----深入理解AOP(面向切面编程)

给程序做增强 事务是最小的执行单元&#xff0c;转账&#xff0c;同时成功、同时失败 TxUtils类式事务管理类&#xff0c;有6个静态方法&#xff0c;可以直接通过类名来调用&#xff0c;threadlocal线程池&#xff0c;还有一个静态代码块&#xff0c;来加载链接 从数据源中获取…

vue源码分析(十)—— 生命周期

文章目录 前言一、关键方法 callHook二、详细的钩子函数说明1.beforeCreate和create2.beforeMount & mounted注意点组件&#xff08;非根组件&#xff09;的渲染节点&#xff08;1&#xff09;invokeInsertHook函数&#xff08;2&#xff09;insert方法&#xff08;3&#…

docker离线安装及部署各类中间件(x86系统架构)

前言&#xff1a;此文主要针对需要在x86内网服务器搭建系统的情况 一、docker离线安装 1、下载docker镜像 https://download.docker.com/linux/static/stable/x86_64/ 版本&#xff1a;docker-23.0.6.tgz 2、将docker-23.0.6.tgz 文件上传到服务器上面&#xff0c;这里放在…

【WIN11新机/重装系统 把尿级系统设置优化】

目录 一、更改鼠标样式二、更改显示器刷新率三、常规文件存储路径0.存储感知1.保存新内容的地方2.快捷访问的文件路径3.Edge浏览器下载路径 四、通知关闭五、开机自启动关闭六、隐私关闭七、性能优化1.开机优化2.用户账控制关闭 八、关闭Windows自动更新九、任务栏设置十、必装…

7.若依参数设置、通知公告、日志管理

参数设置 对系统中的参数进行动态维护。 关闭验证码校验功能 打开页面注册功能 需要修改前端页面代码 通知公告 促进组织内部信息传递 若依只提供了一个半成品&#xff0c;只实现了管理员可以添加通知公告。 日志管理 追踪用户行为和系统运行状况。 登录日志 和操作日志…

修改网络ip地址方法有哪些?常用的有这四种

在数字时代&#xff0c;IP地址作为网络设备的唯一标识&#xff0c;对于网络连接和通信至关重要。然而&#xff0c;有时候我们可能需要修改设备的IP地址&#xff0c;以满足特定的网络需求或解决网络问题。本文将为您详细介绍几种修改网络IP地址的常用方法&#xff0c;无论是对于…

【Java项目】基于SpringBoot的【外卖点餐系统】

【Java项目】基于SpringBoot的【外卖点餐系统】 技术简介&#xff1a;本系统使用JSP技术&#xff0c;采用B/S架构、Spring Boot框架、MYSQL数据库进行开发设计。 系统简介&#xff1a;管理员&#xff1b;首页、个人中心、用户管理、商家管理、菜品分类管理、骑手管理、系统管理…

Spring Boot教程之三十九: 使用 Maven 将 Spring Boot 应用程序 Docker 化

如何使用 Maven 将 Spring Boot 应用程序 Docker 化&#xff1f; Docker是一个开源容器化工具&#xff0c;用于在隔离环境中构建、运行和管理应用程序。它方便开发人员捆绑其软件、库和配置文件。Docker 有助于将一个容器与另一个容器隔离。在本文中&#xff0c;为了将Spring B…

计算机网络|数据流向剖析与分层模型详解

文章目录 一、网络中的数据流向二、计算机网络通信模型1.OSI 模型2.TCP/IP 模型3.TCP/IP五层模型3.1 分层架构描述3.2各层地址结构3.3UDP数据包报头结构 三、总结 一、网络中的数据流向 在计算机网络中&#xff0c;数据的流向是指数据从发送端到接收端的传输路径。数据流向涉及…

uniapp——微信小程序,从客户端会话选择文件

微信小程序选择文件 文章目录 微信小程序选择文件效果图选择文件返回数据格式 API文档&#xff1a; chooseMessageFile 微信小程序读取文件&#xff0c;请查看 效果图 选择文件 /*** description 从客户端会话选择文件* returns {String} 文件路径*/ const chooseFile () &g…

vue3+vite+nginx打包

在开发环境下&#xff0c;已经可以正常地运行一个有增删改查功能的页面了&#xff0c;但如何把它发布到运行服务器呢&#xff1f;仍有许多的问题需要探索。 网上很多文章给了很大的帮助&#xff0c;但总是没有说明原理&#xff0c;对于像我这样的初学者来说&#xff0c;不知其…

CAN201 Introduction to Networking(计算机网络)Pt.2 传输层

文章目录 3. Transport Layer&#xff08;传输层&#xff09;3.1 Multiplexing and demultiplexing&#xff08;多路复用和多路分解&#xff09;3.2 Connectionless transport&#xff1a;UDP3.3 Principles of reliable data transfer3.4 Pipelined communication3.5 TCP: con…

JVM 详解

一. JVM 内存区域的划分 1. 程序计数器 程序计数器是JVM中一块比较小的空间, 它保存下一条要执行的指令的地址. [注]: 与CPU的程序计数器不同, 这里的下一条指令不是二进制的机器语言, 而是Java字节码. 2. 栈 保存方法中的局部变量, 方法的形参, 方法之间的调用关系. 栈又…

Docker--Docker Network(网络)

Docker为什么需要网络管理 容器的网络默认与宿主机及其他容器之间是隔离的,但有时也需要与宿主机和容器进行通信。 容器间及容器与外部通信的需求 容器间通信&#xff1a;在Docker环境中&#xff0c;容器默认与宿主机及其他容器是相互隔离的。然而&#xff0c;在实际应用中&…

深入浅出 MyBatis | CRUD 操作、配置解析

3、CRUD 3.1 namespace namespace 中的包名要和 Dao/Mapper 接口的包名一致&#xff01; 比如将 UserDao 改名为 UserMapper 运行发现抱错&#xff0c;这是因为 UserMapper.xml 中没有同步更改 namespace 成功运行 给出 UserMapper 中的所有接口&#xff0c;接下来一一对…

UE5材质节点Panner

Panner节点可以让贴图动起来&#xff0c;快捷键是P&#xff0c;Speed的数值大小就是贴图移动的快慢&#xff0c;x和y是方向 这个节点可以用来做&#xff0c;传送带&#xff0c;护盾&#xff0c;河流&#xff0c;岩浆&#xff0c;瀑布等 制作岩浆流动效果 创建材质&#xff0c;…

Kimi进行学术方向选择精讲!

目录 1.文献搜索 2.辅助选题 3.选题判断 在我们之前的文章中&#xff0c;小编都强调了选题在文章价值中的核心作用。一篇优秀的文章背后&#xff0c;肯定有一个精心挑选的选题。选题的好坏直接影响着文章能够发表的期刊等级。许多宝子们却采取了相反的做法&#xff0c;将大量…