2024年4月

引言

在现代软件开发领域,多线程并发编程已经成为提高系统性能、提升用户体验的重要手段。然而,多线程环境下的数据同步与资源共享问题也随之而来,处理不当可能导致数据不一致、死锁等各种并发问题。为此,Java语言提供了一种内置的同步机制——
synchronized
关键字,它能够有效地解决并发控制的问题,确保共享资源在同一时间只能由一个线程访问,从而维护程序的正确性与一致性。

synchronized
作为Java并发编程的基础构建块,其简洁易用的语法形式背后蕴含着复杂的底层实现原理和技术细节。深入理解
synchronized
的运行机制,不仅有助于我们更好地利用这一特性编写出高效且安全的并发程序,同时也有利于我们在面对复杂并发场景时,做出更为明智的设计决策和优化策略。

本文将从
synchronized
的基本概念出发,逐步剖析其内在的工作机制,探讨诸如监视器(Monitor)等关键技术点,并结合实际应用场景来展示
synchronized
的实际效果和最佳实践。通过对
synchronized
底层实现原理的深度解读,旨在为大家揭示Java并发世界的一隅,提升对并发编程的认知高度和实战能力。

synchronized是什么?

synchronized
是Java中实现线程同步的关键字,主要用于保护共享资源的访问,确保在多线程环境中同一时间只有一个线程能够访问特定的代码段或方法。它提供了互斥性和可见性两个重要特性,确保了线程间操作的原子性和数据的一致性。

synchronized的特性

synchronized
关键字具有三个基本特性,分别是互斥性、可见性和有序性。

互斥性

synchronized
关键字确保了在其控制范围内的代码在同一时间只能被一个线程执行,实现了资源的互斥访问。当一个线程进入了
synchronized
代码块或方法时,其他试图进入该同步区域的线程必须等待,直至拥有锁的线程执行完毕并释放锁。

可见性

synchronized
还确保了线程间的数据可见性。一旦一个线程在
synchronized
块中修改了共享变量的值,其他随后进入同步区域的线程可以看到这个更改。这是因为
synchronized
的解锁过程包含了将工作内存中的最新值刷新回主内存的操作,而加锁过程则会强制从主内存中重新加载变量的值。

有序性

synchronized
提供的第三个特性是有序性,它可以确保在多线程环境下,对于同一个锁的解锁操作总是先行于随后对同一个锁的加锁操作。这就意味着,通过
synchronized
建立起了线程之间的内存操作顺序关系,有效地解决了由于编译器和处理器优化可能带来的指令重排序问题。

synchronized可以实现哪锁?

有上述synchronized的特性,我们可以知道synchronized可以实现这些锁:

  1. 可重入锁(Reentrant Lock)

    synchronized
    实现的锁是可重入的,这意味着同一个线程可以多次获取同一个锁,而不会被阻塞。这种锁机制允许线程在持有锁的情况下再次获取相同的锁,避免了死锁的发生。
  2. 排它锁/互斥锁/独占锁

    synchronized
    实现的锁是互斥的,也就是说,在同一时间只有一个线程能够获取到锁,其他线程必须等待该线程释放锁才能继续执行。这确保了同一时刻只有一个线程可以访问被锁定的代码块或方法,从而保证了数据的一致性和完整性。
  3. 悲观锁

    synchronized
    实现的锁属于悲观锁,因为它默认情况下假设会发生竞争,并且会导致其他线程阻塞,直到持有锁的线程释放锁。悲观锁的特点是对并发访问持保守态度,认为会有其他线程来竞争共享资源,因此在访问共享资源之前会先获取锁。
  4. 非公平锁:
    synchronized
    在早期的Java版本中,默认实现的是非公平锁,也就是说,线程获取锁的顺序并不一定按照它们请求锁的顺序来进行,而是允许“插队”,即已经在等待队列中的线程可能被后来请求锁的线程抢占。

有关Java中的锁的分类,请参考:
阿里二面:Java中锁的分类有哪些?你能说全吗?

synchronized使用方式

synchronized
关键字可以修饰方法、代码块或静态方法,用于确保同一时间只有一个线程可以访问被
synchronized
修饰的代码片段。

修饰实例方法


synchronized
修饰实例方法时,锁住的是当前实例对象(this)。这意味着在同一时刻,只能有一个线程访问此方法,所有对该对象实例的其他同步方法调用将会被阻塞,直到该线程释放锁。

public class SynchronizedInstanceMethod implements Runnable{

    private static int counter = 0;

    // 修饰实例方法,锁住的是当前实例对象
    private synchronized void add() {
        counter++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {
        SynchronizedInstanceMethod sim = new SynchronizedInstanceMethod();
        Thread t1 = new Thread(sim);
        Thread t2 = new Thread(sim);
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

像上述这个例子,大家在接触多线程时一定会看过或者写过类似的代码,
i++
在多线程的情况下是线程不安全的,所以我们使用
synchronized
作用在累加的方法上,使其变成线程安全的。上述打印结果为:

Final block counter value: 2000

而对于
synchronized
作用于实例方法上时,锁的是当前实例对象,但是如果我们锁住的是不同的示例对象,那么
synchronized
就不能保证线程安全了。如下代码:

public class SynchronizedInstanceMethod implements Runnable{

    private static int counter = 0;

    // 修饰实例方法,锁住的是当前实例对象
    private synchronized void add() {
        counter++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(new SynchronizedInstanceMethod());
        Thread t2 = new Thread(new SynchronizedInstanceMethod());

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

执行结果为:

Final counter value: 1491
修饰静态方法


synchronized
修饰的是静态方法,那么锁住的是类的Class对象,因此,无论多少个该类的实例存在,同一时刻也只有一个线程能够访问此静态同步方法。针对修饰实例方法的线程不安全的示例,我们只需要在
synchronized
修饰的实例方法上加上
static
,将其变成静态方法,此时
synchronized
锁住的就是类的class对象。

public class SynchronizedStaticMethod implements Runnable{

    private static int counter = 0;

    // 修饰实例方法,锁住的是当前实例对象
    private static synchronized void add() {
        counter++;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(new SynchronizedStaticMethod());
        Thread t2 = new Thread(new SynchronizedStaticMethod());

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

执行结果为:

Final counter value: 2000
修饰代码块

通过指定对象作为锁,可以更精确地控制同步范围。这种方式允许在一个方法内部对不同对象进行不同的同步控制。可以指定一个对象作为锁,只有持有该对象锁的线程才能执行被
synchronized
修饰的代码块。

public class SynchronizedBlock implements Runnable{

    private static int counter = 0;

    @Override
    public void run() {
        // 这个this还可以是SynchronizedBlock.class,说明锁住的是class对象
        synchronized (this){
            for (int i = 0; i < 1000; i++) {
                counter++;
            }
        }
    }

    public static void main(String[] args) throws Exception {
        SynchronizedBlock block = new SynchronizedBlock();
        Thread t1 = new Thread(block);
        Thread t2 = new Thread(block);

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter value: " + counter);
    }
}

synchronized
内置锁作为一种对象级别的同步机制,其作用在于确保临界资源的互斥访问,实现线程安全。它本质上锁定的是对象的监视器(Object Monitor),而非具体的引用变量。这种锁具有可重入性,即同一个线程在已经持有某对象锁的情况下,仍能再次获取该对象的锁,这显著增强了线程安全代码的编写便利性,并在一定程度上有助于降低因线程交互引起的死锁风险。

关于如何避免死锁,请参考:
阿里二面:如何定位&避免死锁?连着两个面试问到了!

synchronized的底层原理

在JDK 1.6之前,
synchronized
关键字所实现的锁机制确实被认为是重量级锁。这是因为早期版本的Java中,synchronized的实现依赖于操作系统的互斥量(Mutexes)来实现线程间的同步,这涉及到了从用户态到内核态的切换以及线程上下文切换等相对昂贵的操作。一旦一个线程获得了锁,其他试图获取相同锁的线程将会被阻塞,这种阻塞操作会导致线程状态的改变和CPU资源的消耗,因此在高并发、低锁竞争的情况下,这种锁机制可能会成为性能瓶颈。

而在JDK 1.6中,对synchronized进行了大量优化,其中包括引入了偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)的概念。接下来我们先说一下JDK1.6之前
synchronized
的原理。

对象的组成结构

在JDK1.6之前,在Java虚拟机中,Java对象的内存结构主要有
对象头(Object Header)

实例数据(Instance Data)

对齐填充(Padding)
三个部分组成。

  1. 对象头(Object Header)

    对象头主要包含了两部分信息:Mark Word(标记字段)和指向类元数据(Class Metadata)的指针。Mark Word 包含了一些重要的标记信息,比如对象是否被锁定、对象的哈希码、GC相关信息等。类元数据指针指向对象的类元数据,用于确定对象的类型信息、方法信息等。

  2. 实例数据(Instance Data)

    实例数据是对象的成员变量和实例方法所占用的内存空间,它们按照声明的顺序依次存储在对象的实例数据区域中。实例数据包括对象的所有非静态成员变量和非静态方法。

  3. 填充(Padding)

    在JDK 1.6及之前的版本中,为了保证对象在内存中的存储地址是8字节的整数倍,可能会在对象的实例数据之后添加一些填充字节。这些填充字节的目的是对齐内存地址,提高内存访问效率。填充字节通常不包含任何实际数据,只是用于占位。

JDK1.6之前对象结构.png

对象头

在JDK 1.6之前的Java HotSpot虚拟机中,对象头的基本组成依然包含Mark Word和类型指针(Klass Pointer),但当时对于锁的实现还没有引入偏向锁和轻量级锁的概念,因此对象头中的Mark Word在处理锁状态时比较简单,主要是用来存储锁的状态信息以及与垃圾收集相关的数据。在一个32位系统重对象头大小通常约为32位,而在64位系统中大小通常为64位。
对象头组成部分:

  1. Mark Word(标记字)

    在早期版本的HotSpot虚拟机中,Mark Word主要存储的信息包括:
  • 对象的hashCode(在没有锁定时)。
  • 对象的分代年龄(用于垃圾回收算法)。
  • 锁状态信息,如无锁、重量级锁状态(在使用
    synchronized
    关键字时)。
  • 对象的锁指针(Monitor地址,当对象被重量级锁锁定时,存储的是指向重量级锁(Monitor)的指针)。

HotSpot虚拟机对象头Mark Word.png

对象头中的Mark Word是一个非固定的数据结构,它会根据对象的状态复用自己的存储空间,存储不同的数据。在Java HotSpot虚拟机中,Mark Word会随着程序运行和对象状态的变化而存储不同的信息。其信息变化如下:

image.png

从存储信息的变化可以看出:

  • 对象头的最后两位存储了锁的标志位,01表示初始状态,即未加锁。此时,对象头内存储的是对象自身的哈希码。无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。
  • 当进入偏向锁阶段时,对象头内的标志位变为01,并且存储当前持有锁的线程ID。这意味着只有第一个获取锁的线程才能继续持有锁,其他线程不能竞争同一把锁。
  • 在轻量级锁阶段,标志位变为00,对象头内存储的是指向线程栈中锁记录的指针。这种情况下,多个线程可以通过比较锁记录的地址与对象头内的指针地址来确定自己是否拥有锁。

其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的。重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。

  1. 类型指针(Klass Pointer 或 Class Pointer)

    类型指针指向对象的类元数据(Class Metadata),即对象属于哪个类的类型信息,用于确定对象的方法表和字段布局等。在一个32位系统重大小通常约为32位,而在64位系统中大小通常为64位。

  2. 数组长度(Array Length)
    (仅对数组对象适用):
    如果对象是一个数组,对象头中会额外包含一个字段来存储数组的长度。在一个32位系统中大小通常约为32位,而在64位系统中大小通常为64位。

监视器(Monitor)

在Java中,每个对象都与一个Monitor关联,Monitor是一种同步机制,负责管理线程对共享资源的访问权限。当一个Monitor被线程持有时,对象便处于锁定状态。Java的
synchronized
关键字在JVM层面上通过
MonitorEnter

MonitorExit
指令实现方法同步和代码块同步。
MonitorEnter
尝试获取对象的Monitor所有权(即获取对象锁),
MonitorExit
确保每个MonitorEnter操作都有对应的释放操作。

在HotSpot虚拟机中,Monitor具体由ObjectMonitor实现,其结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //锁计数器,表示重入次数,每当线程获取锁时加1,释放时减1。
    _waiters      = 0, //等待线程总数,不一定在实际的ObjectMonitor中有直接体现,但在管理线程同步时是一个重要指标。
    _recursions   = 0; //与_count类似,表示当前持有锁的线程对锁的重入次数。
    _object       = NULL; // 通常指向关联的Java对象,即当前Monitor所保护的对象。
    _owner        = NULL; // 持有ObjectMonitor对象的线程地址,即当前持有锁的线程。
    _WaitSet      = NULL; //存储那些调用过`wait()`方法并等待被唤醒的线程队列。
    _WaitSetLock  = 0 ; // 用于保护_WaitSet的锁。
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //阻塞在EntryList上的单向线程列表,可能用于表示自旋等待队列或轻量级锁的自旋链表。
    FreeNext      = NULL ; // 在对象Monitor池中可能用于链接空闲的ObjectMonitor对象。
    _EntryList    = NULL ; // 等待锁的线程队列,当线程请求锁但发现锁已被持有时,会被放置在此队列中等待。
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ; // 标志位,可能用于标识_owner是否指向一个真实的线程对象。
  }

其中最重要的就是
_owner

_WaitSet

_EntryList

count
几个字段,他们之间的转换关系:

  1. _owner
    :
    当一个线程首次成功执行
    synchronized
    代码块或方法时,会尝试获取对象的Monitor(即
    ObjectMonitor
    ),并将自身设置为
    _owner
    。该线程此刻拥有了对象的锁,可以独占访问受保护的资源。

  2. _EntryList

    _owner

    :
    当多个线程同时尝试获取锁时,除第一个成功获取锁的线程外,其余线程会进入
    _EntryList
    排队等待。一旦
    _owner
    线程释放锁,
    _EntryList
    中的下一个线程将有机会获取锁并成为新的
    _owner

  3. _owner

    _WaitSet

    :

    _owner
    线程在持有锁的情况下调用
    wait()
    方法时,它会释放锁(即
    _owner
    置为
    NULL
    ),并把自己从
    _owner
    转变为等待状态,然后将自己添加到
    _WaitSet
    中。这时,线程进入等待状态,暂停执行,等待其他线程通过
    notify()

    notifyAll()
    唤醒。

  4. _WaitSet

    _EntryList

    :
    当其他线程调用
    notify()

    notifyAll()
    方法时,会选择一个或全部在
    _WaitSet
    中的线程,将它们从
    _WaitSet
    移除,并重新加入到
    _EntryList
    中。这样,这些线程就有机会再次尝试获取锁并成为新的
    _owner

有上述转换关系我们可以发现,当多线程访问同步代码时:

  1. 线程首先尝试进入_EntryList竞争锁,成功获取Monitor后,将_owner设置为当前线程并将count递增。
  2. 若线程调用wait()方法,会释放Monitor、清空_owner,并将线程移到_WaitSet中等待被唤醒。
  3. 当线程执行完毕或调用notify()/notifyAll()唤醒等待线程后,会释放Monitor,使得其他线程有机会获取锁。

在Java对象的对象头(Mark Word)中,存储了与锁相关的状态信息,这使得任意Java对象都能作为锁来使用,同时,notify/notifyAll/wait等方法正是基于Monitor锁对象来实现的,因此这些方法必须在
synchronized
代码块中调用。

我们查看上述同步代码块
SynchronizedBlock
的字节码文件:

image.png

从上述字节码中可以看到同步代码块的实现是由
monitorenter

monitorexit
指令完成的,其中
monitorenter
指令所在的位置是同步代码块开始的位置,第一个
monitorexit
指令是用于正常结束同步代码块的指令,第二个
monitorexit
指令是用于异常结束时所执行的释放Monitor指令。

关于查看class文件的字节码文件,有两种方式:1、通过命令: javap -verbose <class路径>/class文件。2、IDEA中通过插件:
jclasslib Bytecode viewer

我们再看一下作用于同步方法的字节码:

image.png

我们可以看出同步方法上没有
monitorenter

monitorexit
这两个指令了,而在查看该方法的class文件的结构信息时发现了
Access flags
后边的synchronized标识,该标识表明了该方法是一个同步方法。Java虚拟机通过该标识可以来辨别一个方法是否为同步方法,如果有该标识,线程将持有Monitor,在执行方法,最后释放Monitor。

image.png

总结

synchronized
作用于同步代码块时的原理:
Java虚拟机使用monitorenter和monitorexit指令实现同步块的同步。monitorenter指令在进入同步代码块时执行,尝试获取对象的Monitor(即锁),monitorexit指令在退出同步代码块时执行,释放Monitor。

而对于方法级别的同步的原理:
Java虚拟机通过在方法的访问标志(Access flags)中设置ACC_SYNCHRONIZED标志来实现方法同步。当一个方法被声明为
synchronized
时,编译器会在生成的字节码中插入monitorenter和monitorexit指令,确保在方法执行前后正确地获取和释放对象的Monitor。

本文已收录于我的个人博客:
码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

我在业余时间开发维护了一款免费开源的升讯威在线客服系统,也收获了许多用户。对我来说,只要能获得用户的认可,就是我最大的动力。

客服系统开发过程中,最让我意外的是对 TCP/IP 协议的认识。过去一直认为 TCP/IP 是可靠的连接,加上过去开发的软件网络环境比较稳定,很少在这个问题上纠结。

直到客服系统的客户越来越多,才重新让我认识了基于 TCP/IP 协议的软件应该如何设计开发。

有许多客户做的是外贸业务,服务器部署在海外,比如
香港、韩国、美国
等,有些客服之前用基于网页的客服系统,最为困扰的问题就是丢消息!而使用我的客服系统,做到了
100%稳定,不丢客户不丢消息


演示网络中断,直接禁用网卡,或者手机进入飞行模式,也不丢消息,不出异常。

视频地址:
https://v.youku.com/v_show/id_XNTEwNzQ5Mzg2OA==.html

我会通过一系列的文章详细分析升讯威在线客服系统的并发高性能技术是如何实现的,使用了哪些方案以及具体的做法。本文将介绍如何为多线程处理同步数据。


新版本的宝塔 Docker 与之前有所不同,本文将详细记录在新版宝塔 Docker 中部署升讯威在线客服系统的过程。

新版宝塔 Docker

安装宝塔面板

连接终端后选择不同版本一键安装。

宝塔面板大陆版

https://www.bt.cn
需要中国大陆手机号注册认证

yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh

宝塔面板国际版(aaPanel)

https://www.aapanel.com
无需手机号注册认证

yum install -y wget && wget -O install.sh http://www.aapanel.com/script/install_6.0_en.sh && bash install.sh aapanel

安装完成

安装完成后会显示登录地址、用户名、密码信息。

将登录信息保存下来,然后登录宝塔面板。

安装 Nginx

登录宝塔面板后,勾选并安装以下组件

  • Nginx

客服系统对以上组件的版本没有特殊要求,可自由选择安装较新版本。

如图,勾选左侧的 Nginx 后点击“一键安装”。

安装 Docker 管理器

进入宝塔面板的 “Docker” 画面,点击“立即安装”。

下载并启动升讯威客服系统镜像

获取镜像

进入 Docker 管理画面,选择“本地镜像”标签,点击“从仓库中获取”按钮。

注意:在较早版本的宝塔面板中,进入“软件商店”画面,搜索
Docker
,找到 “Docker管理器” ,点击右侧的 “设置”即可,后续操作步骤与下文类似。

在“docker 官方库”下输入镜像名称
iccb1013/linkup:latest
,点击“拉取”,等待获取镜像完成。

创建容器

在 Docker 管理画面,选择“容器”标签,点击“创建容器”。

在创建容器窗口中,选择“命令创建”,输入以下命令并执行。

docker run -p 8021:8021 -p 8022:8022 -p 8023:8023 -p 9527:9527 -dit --restart=always --privileged=true --name=linkup_latest iccb1013/linkup:latest

注意:在较早版本的宝塔面板中,不支持命令创建容器时,需手动添加端口映射:
8021、8022、8023、9527

并设置执行命令:
-dit --restart=always --privileged=true --name=linkup_latest

容器创建完成,状态显示为“运行中”。

从容器中下载匹配的客服端软件开始使用

  1. 从部署好的站点中下载匹配的客服端软件
    http://kf-resource.yourname.com/shell.zip

  2. 解压后运行 Sheng.Linkup.Client.Shell.exe,并在登录画面中修改服务器地址为你的主程序地址,如
    http://kf-api.yourname.com
    ,然后使用默认账户登录。

默认站点代码 :
freesite
,账户:
kf1
,密码 :
123
登录。

站点代码和客服的账户信息可以登录 Web 管理后台修改。

  1. 打开访客聊天页面,输入内容后点击发送按钮。
    http://kf-resource.yourname.com/WebChat/WebChat.html?sitecode=freesite

即可开始与客服聊天,更详细的使用说明和集成说明,请参阅在线文档的相关章节。

客服端

访客端

简介

升讯威在线客服与营销系统是一款客服软件,但更重要的是一款营销利器。

https://kf.shengxunwei.com/

  • 可以追踪正在访问网站或使用 APP 的所有访客,收集他们的浏览情况,使客服能够主动出击,施展话术,促进成单。
    访* 客端在 PC 支持所有新老浏览器。包括不支持 WebSocket 的 IE8 也能正常使用。
  • 移动端支持所有手机浏览器、APP、各大平台的公众号对接。
  • 支持访客信息互通,可传输访客标识、名称和其它任意信息到客服系统。
  • 具备一线专业技术水平,网络中断,拔掉网线,手机飞行模式,不丢消息。同类软件可以按视频方式对比测试。

希望能够打造:
开放、开源、共享。努力打造 .net 社区的一款优秀开源产品。

钟意的话请给个赞支持一下吧,谢谢~

前言:

上上篇介绍了 IL 指令的分类以及参数加载指令,该加载指令以 Ld 开头,将参数加载到栈中,以便于后续执行操作命令。

上一篇介绍参数存储指令,其指令以 St 开头,将栈中的数据,存储到指定的变量中,以方便后续使用。

本篇将介绍创建实例指令,其指令以 New 开头,用于在运行时动态生成并初始化对象。

创建实例指令简介

在.NET Emit 中,使用 ILGenerator 创建实例是一项重要的操作,它允许我们动态生成对象实例和数组实例的代码。

通过创建实例指令,我们可以在运行时动态生成并初始化对象,为程序提供更大的灵活性和可扩展性。

创建实例指令主要包括 Newobj 指令和 Newarr 指令。

Newobj 指令用于创建新的对象实例,而 Newarr 指令则用于创建新的数组实例。

这些指令的灵活运用可以帮助我们在运行时动态地生成各种类型的实例,满足不同场景下的需求。

在本篇文章中,我们将深入探讨 ILGenerator 中的创建实例指令,详细解析其用法和示例代码。

通过学习本文内容,读者将能够掌握如何利用 ILGenerator 创建对象实例和数组实例,从而更好地理解和应用.NET Emit 技术。

1、创建实例指令:Newobj

对于该指令,其核心在于如何获取构造函数并作为参数传递,下面看一组示例。

共用代码,定义实体(包含无参构造函数、有参构造函数、基本变量):

 public classEntity
{
publicEntity()
{

}
public Entity(intid)
{
this.ID =id;
}
public int ID;}

共用代码,生成程序集,以方便后续对照参考:

AssemblyName assName = new AssemblyName("myAssembly") { Version = new Version("1.1.1.2") };
AssemblyBuilder ab
=AppDomain.CurrentDomain.DefineDynamicAssembly(assName, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder mb
= ab.DefineDynamicModule("myModule", "b.dll");
TypeBuilder tb
= mb.DefineType("MyNameSpace.MyClass", TypeAttributes.Public |TypeAttributes.Class);//定义静态方法 MethodBuilder methodBuilder = tb.DefineMethod("NewObj", MethodAttributes.Public | MethodAttributes.Static, typeof(Entity), newType[] { });ILGenerator il=methodBuilder.GetILGenerator();//il 代码处...... Type classType=tb.CreateType();

ab.Save(
"b.dll");

A、无参数实例化:通过 Type 的 GetConstructor 实例方法获取类型的构造函数。

ILGenerator il =methodBuilder.GetILGenerator();
il.Emit(OpCodes.Newobj,
typeof(Entity).GetConstructor(Type.EmptyTypes));
il.Emit(OpCodes.Ret);
//返回该值

对照生成:

B、使用参数实例化:

 ILGenerator il =methodBuilder.GetILGenerator();
il.Emit(OpCodes.Ldc_I4,
999);
il.Emit(OpCodes.Newobj,
typeof(Entity).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(int) }, null));
il.Emit(OpCodes.Ret);
//返回该值

对照生成:

小说明:

这里构造函数的参数传入,是通过 Ld 系列指令按顺序压入栈中。

2、创建实例指令:Newarr

该指令用于创建数组对象,该指令需要指定数组长度。

A、创建数组:

ILGenerator il =methodBuilder.GetILGenerator();
il.Emit(OpCodes.Ldc_I4,
6);
il.Emit(OpCodes.Newarr,
typeof(Entity));il.Emit(OpCodes.Ret);//返回该值

对照生成代码:

小说明:

Newarr 接收的参数,是 Type 类型。

Newobj 接收的参数,是 ConstructorInfo 构造函数类型。

B、对数组赋值:引用类型

ILGenerator il =methodBuilder.GetILGenerator();//创建数组
il.Emit(OpCodes.Ldc_I4, 3);
il.Emit(OpCodes.Newarr,
typeof(Entity));

il.DeclareLocal(
typeof(Entity[]));
il.DeclareLocal(
typeof(Entity));
il.Emit(OpCodes.Stloc_0);//存储数组
for (int i = 0; i < 3; i++)
{
il.Emit(OpCodes.Newobj,
typeof(Entity).GetConstructor(Type.EmptyTypes));//定义实体类
il.Emit(OpCodes.Stloc_1);//存储实体类

il.Emit(OpCodes.Ldloc_0);
//加载数组 il.Emit(OpCodes.Ldc_I4, i);//加载索引 il.Emit(OpCodes.Ldloc_1);//加载Entity il.Emit(OpCodes.Stelem_Ref);//引用类型赋值 }


il.Emit(OpCodes.Ldloc_0);//加载数组
il.Emit(OpCodes.Ret);
//返回该值

对照生成代码:

C、对数组赋值:值类型

ILGenerator il =methodBuilder.GetILGenerator();//创建数组
il.Emit(OpCodes.Ldc_I4, 3);
il.Emit(OpCodes.Newarr,
typeof(DateTime));

il.DeclareLocal(
typeof(DateTime[]));
il.DeclareLocal(
typeof(DateTime));
il.Emit(OpCodes.Stloc_0);
for (int i = 0; i < 3; i++)
{
//调用 DateTime.Parse 方法创建 DateTime 实例 MethodInfo parseMethod = typeof(DateTime).GetMethod("Parse", new Type[] { typeof(string) });
il.Emit(OpCodes.Ldstr, DateTime.Now.ToString());
//传递当前时间字符串 il.Emit(OpCodes.Call, parseMethod); //调用 Parse 方法 il.Emit(OpCodes.Stloc_1);

il.Emit(OpCodes.Ldloc_0);
//加载数组 il.Emit(OpCodes.Ldc_I4, i);//加载索引 il.Emit(OpCodes.Ldloc_1);//加载Entity il.Emit(OpCodes.Stelem,typeof(DateTime));//赋值 }

il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ret);
//返回该值

对照生成代码:

D、数组取值指令:

总结:

在.NET Emit 入门教程的第六部分中,我们深入探讨了 ILGenerator 指令方法,特别是关于创建实例指令的详细解释。

ILGenerator 是.NET框架中的一个强大工具,用于在运行时生成和执行IL代码。

在这篇文章中,我们学习了如何使用 ILGenerator 来创建实例,其中主要涉及到了两种指令方法:newobj 和 newarr。

通过 newobj 指令,我们可以在IL代码中调用构造函数来创建类的实例,而 newarr 指令则用于创建数组实例。

通过学习这些内容,读者可以更深入地理解 ILGenerator 的使用,并在实际项目中应用动态代码生成的技术。

下一篇,我们将学习方法调用指令的相关内容。

概述

Bean 生命周期管理是 Spring Boot 中的关键功能之一。它负责管理应用程序中的 Java 对象,这些对象被称为 Beans。Spring Boot 通过创建、配置、初始化和销毁这些 Beans 来确保应用程序的正常运行。这个功能的目的是提供一种灵活的方式,使开发人员能够轻松地管理对象的生命周期,从而实现高效的依赖注入和组件化开发。

1、实例化:在 Spring 容器启动时,Spring 会创建配置中定义的所有 Bean。这是通过 Java 类的构造函数或工厂方法来完成的。

2、属性注入:一旦 Bean 被实例化,Spring 会注入所有相关的属性,包括通过构造函数注入或 Setter 方法注入的属性。

3、初始化方法调用:一旦属性被注入,Spring 容器会调用 Bean 的初始化方法。这可以通过@PostConstruct注解、实现InitializingBean接口来完成。初始化过程还提供了一些扩展口:BeanNameAware,BeanFactoryAware,ApplicationContextAware

4、使用 Bean:Bean 现在可以在应用程序中使用,执行其业务逻辑。

5、销毁方法调用:当 Spring 容器关闭时,它会调用 Bean 的销毁方法以释放资源。这可以通过@PreDestroy注解、实现DisposableBean接口来完成。

使用场景

  • 依赖注入:当你需要将一个对象注入到另一个对象中时,Bean 的生命周期管理可以确保被注入的对象已正确初始化。
  • 配置管理:如果你希望根据不同的环境或配置文件来配置 Bean 的属性,Bean 的生命周期管理可以帮助你实现这一目标。
  • 组件扩展:当你需要创建自定义的 Bean,以扩展 Spring Boot 的功能时,Bean 的生命周期管理可以为你提供扩展点。
  • 切面编程:可以在生命周期中预埋代码实现切面编程。例如SpringBoot AOP代理类的实现。

代码演示bean初始化

编写代码演示bean初始化各个节点。

演示代码将创建以下类:

  • TestSupport:一个普通的组件,将作为被观测对象
  • BeanPostProcessorImpl:BeanPostProcessor接口的实现类,用于实现postProcessBeforeInitialization 和postProcessAfterInitialization 并观测TestSupport对象

TestSupport



import com.alibaba.fastjson2.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;

@Slf4j
@RequiredArgsConstructor
@Component
public class TestSupport implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean {

    private final ApplicationContext  applicationContext;
    
    @Override
    public void setBeanName(String s) {
        log.info("TestSupport-BeanNameAware-setBeanName.param={}", s);
    }
    
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        log.info("TestSupport-BeanFactoryAware-setBeanFactory.param={}", "beanFactory");
    }
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        log.info("TestSupport-ApplicationContextAware-setApplicationContext.param={}", JSON.toJSONString(applicationContext.getId()));
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("TestSupport-InitializingBean-afterPropertiesSet");
    }
    
    @PostConstruct
    public void InitSomething() {
        log.info("通过 @PostConstruct 注解执行自定义初始化逻辑");
    }
}



  • BeanNameAware:通过实现BeanNameAware接口并重写setBeanName方法可以获取到bean的name
  • BeanFactoryAware:通过实现BeanFactoryAware接口并重写setBeanFactory方法可以在bean初始化过程中添加额外的扩展,比如可以通过判断beanFactory.alreadyCreated中是否有某bean而做一些不同的操作
  • ApplicationContextAware:通过实现ApplicationContextAware接口并重写setApplicationContext方法可以在bean初始化过程中添加额外扩展。
  • InitializingBean:通过实现InitializingBean接口并重写afterPropertiesSet 方法可以在bean初始化过程中添加额外扩展。比如将此bean存入map中以备它用
  • @PostConstruct:此外还可以通过在方法上面添加@PostConstruct注解来执行一些自定义的业务代码

BeanPostProcessorImpl



import com.ramble.beanlife.support.TestSupport;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class BeanPostProcessorImpl implements BeanPostProcessor {
    /**
     * bean 加工器之在bean实例化之前得预处理
     * 这个处理类针对所有得bean,所以如果需要对特定得bean做处理需要通过 beanName.equals("testSupport")  或者  bean instanceof TestSupport 来过滤
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
//        if (beanName.equals("testSupport")) {
//            log.info("执行  TestSupport 初始化前置方法");
//        }
        if (bean instanceof TestSupport) {
            log.info("bean 加工器捕捉到  TestSupport ,做初始化前置方法");
        }
        // do something
        return bean;
    }
    /**
     * bean 加工器之在bean实例化之后得预处理
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//        if (beanName.equals("testSupport")) {
//            log.info("执行 TestSupport 初始化后置方法");
//        }
        if (bean instanceof TestSupport) {
            log.info("bean 加工器捕捉到  TestSupport ,做初始化后置方法");
        }
        // do something
        return bean;
    }
}




  • postProcessBeforeInitialization:bean 加工器之在bean初始化完毕之前的预处理
  • postProcessAfterInitialization:bean 加工器之在bean初始化之后的预处理
  • 这个扩展点是针对所有的bean的,所以如果需要观测特定的bean在BeanPostProcessor中的情况,需要通过 bean instanceof 或者 beanName equals 进行判断

log


2024-04-07 15:25:33.488  INFO 51628 --- [           main] com.ramble.beanlife.support.TestSupport  : TestSupport-BeanNameAware-setBeanName.param=testSupport
2024-04-07 15:25:35.271  INFO 51628 --- [           main] com.ramble.beanlife.support.TestSupport  : TestSupport-BeanFactoryAware-setBeanFactory.param=beanFactory
2024-04-07 15:25:35.783  INFO 51628 --- [           main] com.ramble.beanlife.support.TestSupport  : TestSupport-ApplicationContextAware-setApplicationContext.param="bean-life-app"
2024-04-07 15:25:36.198  INFO 51628 --- [           main] c.r.b.processor.BeanPostProcessorImpl    : bean 加工器捕捉到  TestSupport ,做初始化前置方法
2024-04-07 15:25:36.651  INFO 51628 --- [           main] com.ramble.beanlife.support.TestSupport  : 通过 @PostConstruct 注解执行自定义初始化逻辑
2024-04-07 15:25:36.652  INFO 51628 --- [           main] com.ramble.beanlife.support.TestSupport  : TestSupport-InitializingBean-afterPropertiesSet
2024-04-07 15:25:38.453  INFO 51628 --- [           main] c.r.b.processor.BeanPostProcessorImpl    : bean 加工器捕捉到  TestSupport ,做初始化后置方法

通过log打印的顺序可以观测到bean初始化的各个扩展口的执行顺序。

代码

https://gitee.com/naylor_personal/ramble-spring-boot/tree/master/bean-life

我们是
袋鼠云数栈 UED 团队
,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:
奇铭

什么是词法分析

要弄清楚什么是词法分析,需要先搞清楚代码是如何执行的。高级编程语言的代码通常需要通过翻译才能被机器执行,而翻译的方式分为两种:

  • 解释: 即以源程序为输入,不产生目标程序,一边解释一边执行,比如 javascript
  • 编译: 即将源程序翻译为机器语言或者目标语言,再执行,比如C, C++

词法分析属于编译的一部分,也是编译的一个阶段。编译通常被分为两个部分:

  1. 编译前端:对源程序进行词法分析,语法分析,语义分析,最终生成中间表现形式,常见的中间表现形式是 AST(抽象语法树)
  2. 编译后端:将中间表现形式(AST)转化为目标程序,比如机器语言或者汇编语言。

在编译前端部分中:

  • 词法分析
    :将源程序分解成一系列的 Token
  • 语法分析
    : token 转换成一个由程序的各种结构(如循环,条件语句等)组成的抽象语法树(Abstract Syntax Tree, AST)。
  • 语义分析
    :确保抽象语法树符合语言规范,例如,确保使用变量前已经被声明,确保函数调用时参数类型和数量与定义时的匹配等。

编译器、解析器、解释器

编译器
的作用将一种语言(通常是高级语言)的源代码转换成另一种语言,其中包含词法分析、 语法分析、语义分析和代码生成等所有编译功能。常见的编译器有
GCC

CLANG
,前端领域最常见的编译器就是
Babel

解析器
则是只负责对源程序进行词法分析和语法分析并将源程序转化为 AST 的部分,但其并不包含语义分析和代码生成功能。前端领域常见的解析器则是
Acorn
,webpack 和 rollup 都适用它作为解析器。

解释器
的任务是读取和执行源代码,而不需要(通常来说)先将源代码转换成机器代码。解释器通常会一边解析源代码一边执行它,或者先将源代码全部解析成AST或某种中间表示形式,然后再执行。Python 和 JavaScript 通常就是以解释执行的方式运行的。

另外,应用广泛的
Antl4
则是一个解析器生成器,它会根据语法文件生成解析器,生成产物中就包含用于词法分析的 Lexer 和 语法分析的 Parser。类似的解析器生成器还有
Bison

Jison

Yacc

Peg.js
。其中
Antlr4

Jison

Peg.js
都可以在 javascript 环境中使用。

Token

源代码中的每一行都是由字符构成的,这些字符可能组成了数字,变量名,关键字等各种元素。词法分析器并不关心元素之间的关系(比如一个变量是否被赋值了,或者是否在使用它之前就被声明了),它只关心将字符逐个归类,生成 token。
一个 token 可以看作是编程语言中的最小有意义的单位,比如一个整数,一个字符串,一个标识符,一个关键字等。一个 token 最少由两部分组成--类型和值。比如
'hello'
的类型为
string
值为
hello

一门典型的语言中 token 类型大概有下面5大类

  1. 关键字: 比如
    IF

    ELSE

    FOR
    等等
  2. 标识符:比如 js 中的变量名,函数名、sql 中的表名、字段名等
  3. 常数/字面量:比如数字字面量-
    100
    ,字符串字面量-
    'hello'
  4. 运算符/操作符:比如
    +

    *

    \

    >
  5. 分隔符/界符:比如
    ;

    {

    (

以一个简单的赋值语句举例

const str = 'hello' + 1;

首先我们需要一张 token 类型对照表,例如:

token tokenType
keyword 1
identifier 2
stringLiteral 3
NumberLiteral 4
operator 5
delimiter 6

那么上述表达式的词法分析结果就应该是:

[
  { type: 1, value: "const" },
  { type: 2, value: "str" },
  { type: 5, value: "=" },
  { type: 3, value: "'hello'" },
  { type: 5, value: "+" },
  { type: 4, value: "1" },
  { type: 6, value: ";" },
]

关键字

关键字即为保留字,比如 ES6 中
break

case

class
等都是保留字,它们不能作为标识符使用,详情请看
javascript 词法文法
。在典型语言中,区分保留字和非保留字的重要依据就是是否能作为标识符,比如
undefind
虽然印象中应该是一个关键字/保留字,但是其实不是,
undefined
也是可以作为标识符的。

标识符

一般来说,在典型语言中,标识符用于标识某种实体,比如在 js 中表示符可以用来标识变量名、函数名,在 sql 中标识符可以用来标识表名、字段名。常见的大多数语言的标识符都由数字,英文字母和下划线组成。上文中提到的关键字虽然也符合这些规则,但是仍然不能作为标识符,因为这会导致难以进行语法分析,容易产生歧义。这也是为什么很多种语言都不支持标识符中包含中划线。 另外在 javascript 中像
TRUE
/
FALSE
此类字面量也不能作为标识符。

空格/换行和注释

对于空格/注释等不影响最终结果的代码片段/字符,一般可以忽略或者暂存,在大部分情况下,它们不需要进入最终的 token 序列中。

词法分析过程

词法分析的过程简单来说就是逐个扫描字符,并根据给定的模式/规则生成 Token 的过程,大概包含以下步骤:

  1. 输入源代码:首先,从文件、命令行或其他来源读取源代码;
  2. 字符扫描:接下来,词法分析器开始从输入流中扫描字符;
  3. 模式匹配:词法分析器试图将扫描到的字符与预定义的模式进行匹配。这些模式常常基于正则表达式定义,并且每个模式对应一种标记(token)类型,在遇到分隔符(如空格或符号)或者匹配了某一种模式时,词法分析器会停止对当前序列的扫描;
  4. 生成标记:一旦找到匹配的模式,词法分析器就生成一个与此模式对应的 Token;
  5. 重复上述过程: 词法分析器再次开始扫描字符,并且尝试找到下一个Token,这个过程会一直持续,直到源代码被完全转换为 Token 序列,或者出现错误;
  6. 错误处理: 如果在分析过程中出现错误,那么词法分析器应当能够在生成的标记序列中添加错误标记或者在编译过程中报告错误;

实现一个简单的词法分析器

根据上文中的词法分析过程,我们来实现一个简单的词法分析器,这个词法分析器能够生成数字,字符串,标识符和一些符号,在此之前我们首先需要定义 token

token 定义

用术语表达,这一步就是种别码定义

enum TOKEN_TYPE {
  number = 'NUMBER',
  string = 'STRING',
  identifier = 'IDENTIFIER',
  punctuation = 'PUNCTUATION',
};

lexer 基本实现

在假设输入的 token 都能被正确识别的情况下使用直接扫描的方式实现如下:

class Lexer {
    constructor(input: string) {
        /** 输入流 */
        this.input = input;
        /** 扫描位置 */
        this.pos = 0;
    }

    input: string;
    pos: number;

    /** 取出当前字符 */
    peek() {
        if (this.pos >= this.input.length) return '<EOF>';
        return this.input[this.pos];
    }

    /** 创建 token */
    createToken(value: unknown, type: TOKEN_TYPE) {
        return { value, type };
    }

    /** generate token */
    nextToken() {
        while (this.pos < this.input.length) {
            if (/\s/.test(this.peek())) {
                this.pos++;
                continue;
            }
            if (/\d/.test(this.peek())) {
                return this.createNumberToken();
            }
            if (this.peek() === '"') {
                return this.createStringToken();
            }
            if (/[a-zA-Z]/.test(this.peek())) {
                return this.createIdentifierToken();
            }
            if (/[{}(),:;+\-*/]/.test(this.peek())) {
                return this.createToken(this.input[this.pos++], TOKEN_TYPE.punctuation);
            }
        }
    }

    createNumberToken() {
        let numStr = '';
        while (/\d/.test(this.peek())) {
            numStr += this.input[this.pos++];
        }
        return this.createToken(numStr, TOKEN_TYPE.number);
    }

    createStringToken() {
        let str = '"';
        this.pos++
        while (this.peek() !== '"' && this.peek() !== '<EOF>') {
            str += this.input[this.pos++];
        }
        str+='"'
        this.pos++
        return this.createToken(str, TOKEN_TYPE.string);
    }

    createIdentifierToken() {
        let idStr = '';
        while (/[a-zA-Z]/.test(this.peek())) {
            idStr += this.input[this.pos++];
        }
        return this.createToken(idStr, TOKEN_TYPE.identifier);
    }
}

// test code
const lexer = new Lexer('let a "Hello, World";123');
const tokenList = [];
let token
while (token = lexer.nextToken()) {
    tokenList.push(token)
}
console.log(tokenList);

// outout
/*
[
  { value: 'let', type: 'IDENTIFIER' },
  { value: 'a', type: 'IDENTIFIER' },
  { value: '"Hello, World"', type: 'STRING' },
  { value: ';', type: 'PUNCTUATION' },
  { value: '123', type: 'NUMBER' }
]
*/

错误恢复

显然上述代码在遇到错误时就无法运行了,所以我们还需要一些错误恢复的机制,当前的 lexer 中的错误大概分为两种

  1. 未知字符
  2. 字符串未正常结束

首先在 tokenType 中新增一个 undefined 类型

enum TOKEN_TYPE {
  undefined = 'UNDEFINED',
};

然后在错误处返回 undefined token

class Lexer {
    // ...
	  nextToken() {
        while (this.pos < this.input.length) {
            // ....
            return this.errorRecovery();
        }
    }
  	// ...
    createStringToken() {
        let str = '"';
        this.pos++
        while (this.peek() !== '"' && this.peek() !== '<EOF>') {
            str += this.input[this.pos++];
        }
        if(this.peek() === '<EOF>'){
            console.warn('Unfinished strings', str)
            return this.createToken(str, TOKEN_TYPE.undefined)
        }
        str+=this.peek();
        this.pos++
        return this.createToken(str, TOKEN_TYPE.string);
    }
  	// ...
    errorRecovery() {
        console.warn('Unexpected character: ' + this.peek());
        const unknownChar = this.peek();
        this.pos++;
        return this.createToken(unknownChar, TOKEN_TYPE.undefined)
    }
}

// test code
const lexer = new Lexer('let a "Hello, World";123');
const tokenList = [];
let token
while (token = lexer.nextToken()) {
    tokenList.push(token)
}
console.log(tokenList);

// output
/*
Unexpected character: =
Unfinished strings "Hello, World
[
  { value: 'let', type: 'IDENTIFIER' },
  { value: 'a', type: 'IDENTIFIER' },
  { value: '=', type: 'UNDEFINED' },
  { value: '"Hello, World', type: 'UNDEFINED' }
]
*/

DFA

在词法分析领域,更常见或者说应用更广的词法分析技术是 DFA (确定有限自动机)。DFA 具有如下优点

  1. 确定性:即在任何状态和任意字符输入下,有且只有一种状态转换
  2. 高效: 只需要一次线性扫描就可以分析完成,不需要回溯
  3. 分析能力:DFA 能够最长匹配和优先匹配关键字,避免了多种符号解析的冲突
  4. 错误检测:如果 DFA 无法从一个状态转换到另一个状态,那么就可以认定存在输入中词法错误,这可以让我们很容易的进行错误检测和恢复。

定义状态

enum STATE_TYPE {
  START = "start", // 初始状态
  NUMBER = "number",
  STRING_OPEN = "string_open",
  STRING_ESCAPE = "string_escape",
  STRING_CLOSE = "string_close",
  IDENTIFIER = "identifier",
  PUNCTUATION = 'punctuation',
  UNKNOWN = "unknown",
}

定义状态转移过程

const TRANSITIONS: Transition[] = [
    // skip whitespace
    { state: STATE_TYPE.START, regex: /\s/, nextState: STATE_TYPE.START },

    /** ==== PUNCTUATION  */
    { 
        state: STATE_TYPE.START,
        regex: /[{}(),:;+\-*/]/,
        nextState: STATE_TYPE.PUNCTUATION,
        tokenType: TOKEN_TYPE.PUNCTUATION
    },
    {
        state: STATE_TYPE.PUNCTUATION,
        regex: /[\w\W]/,
        nextState: STATE_TYPE.START,
        tokenType: TOKEN_TYPE.PUNCTUATION
    },

    /** ==== identifier  */
    {
        state: STATE_TYPE.START,
        regex: /[a-z_A-Z]/,
        tokenType: TOKEN_TYPE.IDENTIFIER,
        nextState: STATE_TYPE.IDENTIFIER,
    },
    {
        state: STATE_TYPE.IDENTIFIER,
        regex: /[a-z_A-Z]/,
        tokenType: TOKEN_TYPE.IDENTIFIER,
        nextState: STATE_TYPE.IDENTIFIER,
    },
    {
        state: STATE_TYPE.IDENTIFIER,
        regex: /[^a-z_A-Z]/,
        tokenType: TOKEN_TYPE.IDENTIFIER,
        nextState: STATE_TYPE.START,
    },

    /** ===== number */
    {
        state: STATE_TYPE.START,
        regex: /[0-9]/,
        tokenType: TOKEN_TYPE.NUMBER,
        nextState: STATE_TYPE.NUMBER,
    },
    {
        state: STATE_TYPE.NUMBER,
        regex: /[0-9]/,
        tokenType: TOKEN_TYPE.NUMBER,
        nextState: STATE_TYPE.NUMBER,
    },
    {
        state: STATE_TYPE.NUMBER,
        regex: /[^0-9]/,
        tokenType: TOKEN_TYPE.NUMBER,
        nextState: STATE_TYPE.START,
    },

    /** ===== string */
    {
        state: STATE_TYPE.START,
        regex: /"/,
        tokenType: TOKEN_TYPE.UNDEFINED,
        nextState: STATE_TYPE.STRING_OPEN,
    },
    {
        state: STATE_TYPE.STRING_OPEN,
        regex: /[^"]/,
        tokenType: TOKEN_TYPE.UNDEFINED,
        nextState: STATE_TYPE.STRING_ESCAPE,
    },
    {
        state: STATE_TYPE.STRING_ESCAPE,
        regex: /[^"]/,
        tokenType: TOKEN_TYPE.UNDEFINED,
        nextState: STATE_TYPE.STRING_ESCAPE,
    },
    {
        state: STATE_TYPE.STRING_ESCAPE,
        regex: /"/,
        tokenType: TOKEN_TYPE.STRING,
        nextState: STATE_TYPE.STRING_CLOSE,
    },
    {
        state: STATE_TYPE.STRING_CLOSE,
        regex: /[\w\W]/,
        tokenType: TOKEN_TYPE.STRING,
        nextState: STATE_TYPE.START,
    },
];

状态机

class StateMachine {
    constructor() {
        this.transitions = TRANSITIONS;
    }

    transitions: Transition[];

    addTransition(transition: Transition) {
        this.transitions.push(transition);
    }

    performTransition(currentState: STATE_TYPE, char: string) {
        const transition = TRANSITIONS.find(
            (t) => t.state === currentState && t.regex.test(char)
        );
        // 遇到未知字符串时,直接回到开始状态
        return (
            transition ?? {
                state: STATE_TYPE.UNKNOWN,
                regex: /./,
                tokenType: TOKEN_TYPE.UNDEFINED,
                nextState: STATE_TYPE.START,
            }
        );
    }
}

词法分析器

class Lexer {
    constructor(input: string) {
        this.currentState = STATE_TYPE.START;
        this.input = input;
        this.pos = 0;
        this.stateMachine = new StateMachine();
    }
    stateMachine: StateMachine;
    currentState: STATE_TYPE;
    input: string;
    pos: number;

    peek() {
        if (this.pos >= this.input.length) return "<EOF>";
        return this.input[this.pos];
    }

    createToken(value: unknown, type: TOKEN_TYPE) {
        return { value, type };
    }

    nextToken() {
        let buffer = ""; // 缓冲区
        let tokenType: TOKEN_TYPE | undefined = undefined;
        while (this.pos < this.input.length) {
            const transition = this.stateMachine.performTransition(
                this.currentState,
                this.peek()
            );

            this.currentState = transition.nextState;
            tokenType = transition.tokenType;

            if(transition.nextState !== STATE_TYPE.START) {
                buffer += this.peek();
                this.pos++;
                continue;
            }

            if(!transition.tokenType) {
                buffer = '';
                this.pos++;
                continue; 
            }

            if(transition.state === STATE_TYPE.UNKNOWN) {
                buffer += this.peek();
                this.pos++;
            }

            return this.createToken(buffer, transition.tokenType)
        }

        if(tokenType && buffer) {
            return this.createToken(buffer, tokenType)
        }
    }
}

最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star