科普文:Lombok使用及工作原理详解

news2024/11/10 13:42:31

1. 概叙

Lombok是什么?

  1. Project Lombok 是一个 JAVA 库,它可以自动插入编辑器和构建工具,为您的 JAVA 锦上添花。
  2. 再也不要写另一个 getter/setter 或 equals 等方法,只要有一个注注解,你的类就有一个功能齐全的生成器,自动记录变量,等等。

官方地址:https://projectlombok.org/

Lombok是一种Java™实用工具,可用来帮助开发人员消除Java的冗长代码,尤其是对于简单的Java对象(POJO)。它通过注释实现这一目的

首先Lombok是一款Java IDE的应用工具插件,一个可以通过简单的注解形式来帮助我们简化消除一些必须有但显得很臃肿的Java代码的工具,比如属性的构造器、getter、setter、equals、hashcode、toString方法。结合IDE,通过使用对应的注解,可以在编译源码的时候生成对应的方法。

通过在开发环境中实现Lombok,开发人员可以节省构建诸如hashCode()和equals()这样的方法以及以往用来分类各种accessor和mutator的大量时间。

虽然上述的那些常用方法IDE都能生成,但是lombok更加简洁与方便,能够达到的效果就是在源码中不需要写一些通用的方法,但是在编译生成的字节码文件中会帮我们生成这些方法,这就是lombok的神奇作用。

lombok插件安装

使用的IDE是Intellij idea,编译器需要在

preference->plugins->Browse repositories

搜索lombok,然后安装plugins,需要稍等片刻

添加jar包

在项目中添加lombok的jar包,笔者用的是maven,所以在pom文件中添加了如下的依赖。gradle使用见官网。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.16</version>
    <scope>provided</scope>
</dependency>

常用的注解

官网注解介绍:Stable

2. Lombok原理介绍

2.1 Java类文件编译过程

首先,我们知道 Lombok 功能是作用在类编译时期,那我们来看下一个类编译的过程。

  1. 定义一个 PersonDTO.Java 类

代码语言:javascript

复制

public class PersonDTO {
  //姓名
  private String name;  
}
  1. Javac PersonDTO.Java 对源代码进行解析转化,会生成一棵抽象语法树( AST );
  2. 运行过程中会调用实现了 JSR 269 注解处理器,下面介绍;
  3. JSR 实现可处理自定义逻辑,包括可修改编译后的抽象语法树(AST);
  4. Javac 使用修改后的抽象语法树(AST)生成字节码文件;

过程如下图:

AST 是抽象语法树(Abstract Syntax Tree) 的缩写,是 JAVA 源代码展示的一种树状结构它将代码的结构和语法元素映射到树节点上,使得程序可以在编译、

分析和转换过程中更容易地操作和理解。有兴趣可以学习 JavaParser 源码, 了解将 Java 源代码解析生成成一个抽象语法树( AST ),这个树形结构表示了代码的

语法结构包括类、方法、变量、语句等等过程。

github地址:https://github.com/javaparser/javaparser.

如: PersonDTO.Java 在 idea 中使用可视化工具展示文件 AST 树

2.2 JSR 269介绍

首先 JSR 269全称" Pluggable Annotation Processing API ",是 JAVA 平台的一项规范,也被称之为注解处理器 API 。在Java6引入,用于在编译时处理

注解,目标是提供更丰富的编译时元数据处理能力,以增强Java编译器的功能。这个规范允许开发人员创建自定义的注解处理器,这些处理器可以在编译时检查、分析和生成Java代码。

JSR 269在JDK6中被引入,作为APT的替代方案。javac执行的时候会调用Pluggable Annotation Processing API,因此我们可以通过实现此API来改变编译期的一些行为从而达到目的。具体的编译流程如下:

javac编译流程

举例来说,现在有一个实现了Pluggable Annotation Processing API的程序A,那么使用javac编译时的具体流程如下:

1. javac编译器对源码进行分析,生成一个抽象的语法树(AST)

2. javac编译器运行A程序

3. A程序完成逻辑,一般是修改此语法树

4. javac使用修改后的语法树生成可执行的字节码文件

Lomok便是通过Pluggable Annotation Processing API来实现代码生成的。

应用框架:

  1. Servlet、JAX-RS(RESTful Web服务)JSR 269 来生成用于处理 HTTP 请求的代码。
  2. Spring Boot 项目中以处理各种自定义注解,如 @Controller、@Service、@Repository 等。这些注解可以用于自动化配置、依赖注入等方面。
  3. Hibernate 它使用 JSR 269 来处理 JPA 注解,并生成与数据库交互的代码。
  4. Lombok 是一个 JAVA 库,它通过注解处理器生成常见的 JAVA 代码,如 getter、setter、equals、hashCode 等,以简化开发工作。
  5. MapStruct 是一个用于对象映射的 JAVA 库,它使用 JSR 269 来生成类型安全的映射代码,帮助开发人员将一个对象映射到另一个对象。

如何实现自定义注解注解处理器:

1.声明自定义注解;如 Lombok 下的 @Data,@Getter,@Setter等。

2.实现 Process接口,或者继承 AbstractProcessor 复写 process 方法,处理自定义注解逻辑。

代码语言:javascript

复制

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import java.util.Set;

@SupportedAnnotationTypes("com.example.MyAnnotation") // 自定义注解
@SupportedSourceVersion(SourceVersion.RELEASE_8) //支持的Java版本
public class MyAnnotationProcessor extends AbstractProcessor {
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)  {
    // 在这里处理自定义注解,生成代码或执行其他任务
    return true;
  }
}

3.注册注解处理器

两种方式:

  1. Resource 文件:项目 META-INF/services 创建 javax.annotation.processing.Processor 文件,自定义注解处理器的全类名写到此文件中。
  2. 通过谷歌工具包 auto-service ,可自动生成以上配置文件。

代码语言:javascript

复制

<!-- 编译注解执行注册 jar包-->
<dependency>
  <groupId>com.google.auto.service</groupId>
  <artifactId>auto-service</artifactId>
  <version>1.0.1</version>
</dependency> 

@AutoService(Processor.class) //谷歌工具包方式:注册注解处理器
@SupportedAnnotationTypes("com.example.MyAnnotation") // 自定义注解
@SupportedSourceVersion(SourceVersion.RELEASE_8) //支持的Java版本
public class MyAnnotationProcessor extends AbstractProcessor {

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
 {
    // 在这里处理自定义注解,生成代码或执行其他任务
    return true;
  }
}

2.3 Lombok 实现原理

1. Lombok 实际就是结合注解处理器和 AST 技术, Lombok 实现的注解处理器会遍历 AST ,查找与 Lombok 注解相关的元素,根据注解的要求生成新的代码。

2.编译前后的 AST 语法树对比

加入@Getter注解编译后

3. Lombok 注解处理器,采用 Resource 方式注册编译注解处理器

注解处理器 AnnotationProcessor 源码:

代码语言:javascript

复制

class AnnotationProcessorHider {

 public static class AstModificationNotifierData {
   public volatile static boolean lombokInvoked = false;
 }
 
 public static class AnnotationProcessor extends AbstractProcessor {
  // 获取支持的注解类型
  @Override
  public Set<String> getSupportedOptions() {
   return instance.getSupportedOptions();
  }

  // 获取支持的注解类型
  @Override
  public Set<String> getSupportedAnnotationTypes() {
   return instance.getSupportedAnnotationTypes();
  }

  // 支持的JDK版本
  @Override
  public SourceVersion getSupportedSourceVersion() {
   return instance.getSupportedSourceVersion();
  }

  //初始化环境
  @Override
  public void init(ProcessingEnvironment processingEnv) {
   disableJava9SillyWarning();
   AstModificationNotifierData.lombokInvoked = true;
   instance.init(processingEnv);
   super.init(processingEnv);
  }

  // 处理自定义注解逻辑
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment 
   roundEnv) {
   return instance.process(annotations, roundEnv);
  }
 }

自定义注解处理器 Handler : 在 Jar 包的 lombok.javac.handlers下,每个注解处理对应一个 Handler. 如 HadlerGetter.java 操作 AST 树生成 getter 方法.

2.4手动实现一个 @Getter 功能

2.4.1.创建 maven 工程 demo 包含两个子模块 getter/getter-use

2.4.2. getter 工程

pom文件:

代码语言:javascript

复制

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>demo</artifactId>
    <groupId>com.example</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  <artifactId>getter</artifactId>

  <dependencies>
    <!-- 注解执行器依赖jar -->
    <dependency>
      <groupId>com.sun</groupId>
      <artifactId>tools</artifactId>
      <version>1.6.0</version>
      <scope>system</scope>
      <systemPath>${java.home}/../lib/tools.jar</systemPath>
    </dependency>

    <!-- 编译注解执行注册 -->
    <dependency>
      <groupId>com.google.auto.service</groupId>
      <artifactId>auto-service</artifactId>
      <version>1.0.1</version>
    </dependency>
  </dependencies>
</project>

自定义 GetterTest 注解

代码语言:javascript

复制

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author zcy1
 */
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface GetterTest {
}

GetterTest 编译注解处理器 GetterProcessor

代码语言:javascript

复制

import com.google.auto.service.AutoService;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

/**
 * @author zcy1
 */
@AutoService(Processor.class) //谷歌工具包方式:注册注解处理器
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("GetterTest")
public class GetterProcessor extends AbstractProcessor {

  /**
   * 用于在编译器打印消息的组件
   */
  private Messager messager;
  
  /**
   * 提供待处理抽象语法树
   */
  private JavacTrees trees;
  
  /**
   * 用来构造语法树节点
   */
  private TreeMaker treeMaker;
  
  /**
   * 用于创建标识符的对象
   */
  private Names names;
  
  /**
   * 获取一些注解处理器执行处理逻辑时一些关键对象
   * @param processingEnv 处理环境
   */
  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    this.messager = processingEnv.getMessager();
    this.trees = JavacTrees.instance(processingEnv);
    Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
    this.treeMaker = TreeMaker.instance(context);
    this.names = Names.instance(context);
  }
  
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
 {
    // 获取自定义GetterTest注解的类
    Set<? extends Element> elementsAnnotatedWith = 
   roundEnv.getElementsAnnotatedWith(GetterTest.class);
    elementsAnnotatedWith.forEach(e -> {
      JCTree tree = trees.getTree(e);
        tree.accept(new TreeTranslator() {
          @Override
          public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
            List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
              // 在抽象树中找出所有的成员变量
              for (JCTree jcTree : jcClassDecl.defs) {
                if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
                  JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
                    jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                 }
              }
              // 对于变量进行生成getter方法的操作
              jcVariableDeclList.forEach(jcVariableDecl -> {
                messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " 
         has been processed");
                  jcClassDecl.defs = 
          jcClassDecl.defs.prepend(createGetterMethod(jcVariableDecl));
              });
              super.visitClassDef(jcClassDecl);
              }
          });
      });
      return true;
  }
  
  private JCTree.JCMethodDecl createGetterMethod(JCTree.JCVariableDecl jcVariableDecl) {
    ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
   
    // 生成表达式 this.name = name
    JCTree.JCExpressionStatement aThis = 
   makeAssignment(treeMaker.Select(treeMaker.Ident(names.fromString("this")),               jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));        
    statements.append(aThis);
    JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

    // 生成入参 (String name)
    JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER),
            jcVariableDecl.getName(), jcVariableDecl.vartype, null);
    List<JCTree.JCVariableDecl> parameters = List.of(param);

    // 生成返回对象 void
    JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());

       //生成方法名 getName
    Name methodName = getMethodName(jcVariableDecl.getName());

    // 返回语法树对象
  return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),methodName, methodType,   
  List.nil(),parameters, List.nil(), block, null);
  
  }
  
  /**
   * 驼峰方法名
   */
  private Name getMethodName(Name name) {
    String s = name.toString();
    return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, 
   name.length()));
  }
  
  private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, 
  JCTree.JCExpression rhs) {
    return treeMaker.Exec(treeMaker.Assign(lhs, rhs));
  }
}
2.4.4 getter-use工程

pom 文件 引入getter工程

代码语言:javascript

复制

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>demo</artifactId>
    <groupId>com.example</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>getter-use</artifactId>

  <dependencies>
    <dependency>
    <groupId>com.example</groupId>
    <artifactId>getter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    </dependency>
  </dependencies>
</project>

PersonDTO 使用@GetterTest

代码语言:javascript

复制

@GetterTest
public class PersonDTO {
  private String name;

  private Integer age;
}
2.5.5 项目进行编译

getter 模块下自动生成注册器

getter-use 模块下 PersonDTO.class 可见生成了对应属性的 get 方法

3.总结

本文通过以上对 Lombok 相关介绍,通过对 JAVA 文件编译过程分析和 JSR269 实现的方式, 基于这个规范然后引申出 Lombok 实现原理过程介绍,以及手动实现 getter 案例,想必我们对 Lombok 原理也有了相应的了解。虽然 Lombok 提供了许多便利,由于生成的代码不在源文件中可见,就会导致代码的可读性和维护性较差。在工作中 Lombok 使用时注意闭坑:

问题

解决

@Data 和 @Builder 一起使用时,无参构造方法会被干掉

手动加上注解: @AllArgsConstructor、@NoArgsConstructor。

@Builder 导致类属性默认值无效。

有默认值属性上加注解 : @lombok.Builder.Default。

@Data 生成的 toString 方法,默认能不输出父类属性

子类添加: @ToString(callSuper = true)。

参考文献

Lombok 官网地址: https://projectlombok.org

JavaParser 源码地址: https://github.com/javaparser/javaparser

JAVA 抽象语法树 AST 浅析与使用: https://www.freesion.com/article/4068581927/

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

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

相关文章

《动手做科研》08. 云端上的深度学习

地址链接:《动手做科研》08. 云端上的深度学习 欢迎加入我的知识星球&#xff0c;定期分享AI论文干货知识&#xff01; 导读: 当我们开始开发复杂模型时&#xff0c;尝试在本地计算机上训练模型通常不是一个可行的选择&#xff0c;因为我们本地的显存都比较受限制&#xff0c;而…

streamlit安装成功

文章目录 安装streamlit方法1:联网在线安装方法2:离线安装(无网络)安装streamlit 方法1:联网在线安装 在Anaconda的工作环境(或其他虚拟环境或真是环境,均可以)中,安装streamlit: pip install streamlit安装成功后如下图所示: 然后我们测试一下streamlit是否安装…

一文看懂什么是架构

对程序员来说&#xff0c;架构是一个常见词汇。如果想成为一名架构师&#xff0c;对架构概念的理解必须清晰。否则&#xff0c;在制定架构方案时&#xff0c;肯定会漏洞百出&#xff0c;问题频发&#xff0c;这将对你的面试、晋升和团队领导产生负面影响。 我们看下维基百科关…

地方坐标系CAD如何转成标准的国家2000

0序&#xff1a; 现在基本全国范围内都在逐步的应用国家2000了。不分设计院可能由于项目等原因&#xff0c;还在使用地方坐标系&#xff0c;这些地方坐标系的数据很难和标准地图进行叠加。 有些软件提供了4参数的方法来接入地方坐标系的CAD。但不是所有的软件&#xff0c;平台…

用manim创建坐标系

用manim创建坐标系 1.Axes._update_default_configs()的使用 构造函数&#xff1a; static _update_default_configs(default_configs, passed_configs) manim 是一个用于创建数学动画的 Python 库。static _update_default_configs(default_configs, passed_configs) 是 man…

每日一题 ~乘积最大子数组

. - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/maximum-product-subarray/description/ 题目分析 题目要求找出给定整…

车载A2B芯片AD2428 IIC功能介绍

车载A2B芯片AD2428 IIC功能介绍 一&#xff0c;功能描述二&#xff0c;架构概念2.1 I2C接口2.2 I2C时钟拉伸2.3 收发器I2C接口2.4 收发器I2C访问延迟 一&#xff0c;功能描述 A2B收发器在节点之间的距离上连接多通道I2S&#xff08;IC间声音&#xff09;同步脉冲编码调制&…

Realize LIVE 2024 | 庭田科技参与2024西门子用户大会

7月24日至25日&#xff0c;西门子“2024大中华区Realize LIVE用户大会”在上海盛大开幕。作为西门子在工业软件领域的年度盛会&#xff0c;此次大会不仅是一场科技的盛宴&#xff0c;更是一次探索工业未来、推动行业数智化转型的深度对话。大会以“让数字转型立现真章”为主题&…

Sonar-Scanner: 静态代码分析的利器

Sonar-Scanner: 静态代码分析的利器 懂得享受生活的过程&#xff0c;人生才会更有乐趣。每个人都会遇到一些陷阱&#xff0c;每个人都有过去&#xff0c;有的甚至是失败的往事。过去的错误和耻辱只能说明过去&#xff0c;真正能代表人一生的&#xff0c;是他现在和将来的作为。…

EmoBench:评估大模型的情感智能

人工智能咨询培训老师叶梓 转载标明出处 情感智能是识别、理解和管理自己和他人的情感的能力&#xff0c;对于塑造人际关系、改善决策和影响整体福祉至关重要。尽管情感智能系统被认为具有相似的好处&#xff0c;但现有研究主要集中于情绪识别&#xff0c;忽视了情绪调节和通过…

传知代码-上下位关系自动检测方法(论文复现)

代码以及视频讲解 本文所涉及所有资源均在传知代码平台可获取 概述 本文复现论文 Hearst patterns revisited: Automatic hypernym detection from large text corpora[1] 提出的文本中上位词检测方法。 在自然语言处理中&#xff0c;上下位关系&#xff08;Is-a Relations…

单链表的应用(附代码)

链表 链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的。其实链表可以想象为小火车&#xff0c;链表比顺序表具有更好的灵活性&#xff0c;只需要通过指针的改变就可以实现增删查改。 这是逻辑思维下链表的样…

使用TensorRT对YOLOv8模型进行加速推理

这里使用GitHub上shouxieai的 infer框架 对YOLOv8模型进行加速推理&#xff0c;操作过程如下所示&#xff1a; 1.配置环境&#xff0c;依赖项&#xff0c;包括&#xff1a; (1).CUDA: 11.8 (2).cuDNN: 8.7.0 (3).TensorRT: 8.5.3.1 (4).ONNX: 1.16.0 (5).OpenCV: 4.10.0 2.clon…

redis:Linux安装redis,redis常用的数据类型及相关命令

1. 什么是NoSQL nosql[not only sql]不仅仅是sql。所有非关系型数据库的统称。除去关系型数据库之外的都是非关系数据库。 1.1为什么使用NoSQL ​ NoSQL数据库相较于传统关系型数据库具有灵活性、可扩展性和高性能等优势&#xff0c;适合处理非结构化和半结构化数据&#xff0c…

服务运营|摘要:INFORMS 近期收益管理(Revenue Management )相关文章

编者按&#xff1a; 本期涵盖了INFORMS与收益管理相关的文章及其基本信息。 Title: Online Learning for Constrained Assortment Optimization Under Markov Chain Choice Model 基于马尔可夫链选择模型的约束下选品优化的在线学习 Link: https://pubsonline.informs.org/do…

召唤生命,阻止轻生——《生命门外》

本书的目的&#xff0c;就是阻止自杀&#xff01;拉回那些深陷在这样的思维当中正在挣扎犹豫的人&#xff0c;提醒他们珍爱生命&#xff0c;让更多的人&#xff0c;尤其是年轻人从执迷不悟的犹豫徘徊中幡然醒悟&#xff0c;回归正常的生活。 网络上抱孩子跳桥轻生的母亲&#…

Linux中gdb调试器的使用

Linux调试器&#xff1a;gdb gdb简介基本使用和常见的指令断点相关运行相关命令 gdb简介 我们都知道一个程序一般有两个版本分别是debug&#xff0c;和release版本&#xff0c;后者就是发布给用户的版本&#xff0c;而前者就是我们程序员用来调试用的版本。 他们有什么区别呢&…

Docker搭建Mysql主从复制,最新,最详细

Docker搭建Mysql主从复制&#xff0c;最新&#xff0c;最详细 这次搭建Mysql主从复制的时候&#xff0c;遇到不少问题&#xff0c;所以本次重新记录一下&#xff0c;使用Docker搭建一主三从的Mysql 一、Docker-Compose创建4个Mysql容器 1.1 创建对应的映射文件夹和对应的配置…

GitLab的安装步骤与代码拉取上传操作

一、GitLab的安装 详情见如下博客链接&#xff1a;gitlab安装 二、GitLab配置ssh key &#xff08;1&#xff09;打开Git Bash终端生成SSH和添加步骤 1、全局配置git用户名 git config --global user.name "xxx"注意&#xff1a;xxx为你自己gitlab的名字 2、全局…

JavaScript递归菜单栏

HTML就一个div大框架 <div class"treemenu"></div> 重中之重的JavaScript部分他来啦&#xff01; 注释也很清楚哟家人们&#xff01; let data; let arr []; let cons;let xhr new XMLHttpRequest(); // 设置请求方式和请求地址 xhr.open(get, ./js…