2024年10月

本文主要谈谈Kafka用于实时数据通道场景的缺陷,以及如何在架构上进行弥补。

Kafka归属于消息队列类产品,其他竞品还有RabbitMQ、RocketMQ等,总的来说它们都是基于生产者、中介和消费者三种角色,提供高并发、大数据量场景下的消息传递。Kafka诞生自Hadoop生态,与生态中的其他组件具有更好的亲和性,在实时数据场景中往往是首选。随着数据实时应用的需求高涨,Kafka作为构建实时数据通道的核心组件,得到了广泛的应用。

Kafka本身不介入消息内容,需要生产者和消费者事先约定某种通讯契约(包括序列化框架和数据结构两部分)来编码和解码消息内容。这个通讯契约由参与双方系统约定而成,双方是对等关系,一旦发生变化需要双方重新协商。

对于消息队列场景,上述机制完全没问题。但在实时数据场景下,数据往往由生产侧CDC工具以抓取数据库的方式产生,那么通讯契约中的数据结构部分直接采用了生产系统的表结构,即由生产侧系统单方面定义的,对下游具有强制性。而且,当生产系统的表结构变化时,下游也不得不适配全表结构的变化,即使只需要部分字段的数据。可见,实时数据场景下,下游系统完全是从属关系,产生了大量冗余工作量。另外,表结构变更传递到下游系统,并没有自动化机制,容易产生时间延迟和沟通误差等问题。

Kafka作为一个实时数据的汇集点,并不能对上述两个问题进行有效控制,也就是本文所说的缺陷。

关于解决方案,首先是在Kafka上增加元数据管理模块,在实践中我们选择了Schema Registry,由confulent开源的元数据管理工具。整体架构如下图所示

每个topic都有schema,且随着topic中数据结构的变化,schema会产生多个版本,每个版本的schema具有全局唯一id。一条完整的消息就由schema id和data两部分构成,在消费端读取消息时可以根据id找回schema,进而解析消息。

可见,引入SR后系统具备了在Kafka通道中获取上游系统表结构继而解析消息的能力。当表结构发生变化时,CDC工具会自动推送schema给SR。市场上主流的CDC工具,如Oracel Golden Gate(OGG),已经提供了对Schema Registry的适配。

这样,我们解决了schema在上下游之间自动更新同步的问题。

在此基础上,我们又增加了对表结构的裁剪能力,即可以基于不同下游系统的需求对同一个topic进行差异化的读取字段内容。而裁剪后,也就形成了一个上下游对等关系的契约,降低了下游系统的无效耦合,从而消除了冗余工作量。更重要的是,裁剪的过程是零编码的,仅在交互界面上点选操作即可。这个裁剪工具并没有找到开源实现版本,所以我们自己进行了研发,取名为schema manager。

最后,我们基于schema registry和schema manager,开发了自适应的消息解析程序,封装为SDK。这样下游系统只需要按照SDK接口(兼容Kafka原生接口)订阅消息,即可完全屏蔽掉无关的上游变更内容,对上述一套实现机制完全无感。

最后,简单总结下答案,实时数据通道的四个能力:

  • Kafka的消息队列能力
  • 与生产侧打通的schema自动更新和管理能力
  • 面向消费侧需求的schema裁剪能力
  • 自适应schema变更的解析能力

通过这样的实时数据通道,上下游系统恢复到了对等通讯关系,基本清除了下游的冗余工作量。

在算法比赛中,快读是一个常用的技巧,用于提高输入数据的速度。常见的快读方法有以下几种:

1.
C++ 中的快读

C++ 中常用
scanf

getchar
进行快读。

#include <cstdio>
#include <cstring>

inline int read() {
    int x = 0, f = 1;
    char c = getchar();
    while (c < '0' || c > '9') {
        if (c == '-') f = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9') {
        x = x * 10 + c - '0';
        c = getchar();
    }
    return x * f;
}

2.
Python 中的快读

Python 的
input()
相对较慢,可以用
sys.stdin.read
来提高速度。

import sys

input = sys.stdin.read
data = input().split()

3.
Java 中的快读

Java 中可以使用
BufferedReader

StringTokenizer

import java.io.*;
import java.util.StringTokenizer;

public class FastReader {
    BufferedReader br;
    StringTokenizer st;

    public FastReader() {
        br = new BufferedReader(new InputStreamReader(System.in));
    }

    String next() {
        while (st == null || !st.hasMoreTokens()) {
            try {
                st = new StringTokenizer(br.readLine());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return st.nextToken();
    }

    int nextInt() {
        return Integer.parseInt(next());
    }
}

4.
总结

快读的原理主要基于减少输入操作的次数和使用更高效的输入方法,从而提高程序的整体性能。以下是几个关键点:

1.
减少系统调用

  • 在标准输入中,每次调用
    scanf

    input()
    都会进行一次系统调用,这会耗费时间。快读通过一次性读取大量数据并在内存中处理来减少这种调用。

2.
使用缓冲区

  • 快读通常使用缓冲区来临时存储输入数据,利用内存的读取速度远高于逐个字符的读取。比如,通过
    getchar

    BufferedReader
    等方法,一次性读取整个行或多行数据。

3.
字符串处理

  • 读取数据后,通常会将其存储为字符串,然后通过分隔符(如空格、换行)进行解析。这样可以快速提取所需的数据,而不需要每次都调用输入函数。

4.
字符处理

  • 快读通常采用字符处理的方式,比如逐个字符读取直到找到数字或特定格式,能有效地处理整数或浮点数等基本数据类型。

5.
避免类型转换

  • 在一些实现中,可以将输入的字符直接转换为数字,减少了使用函数如
    atoi
    的时间开销,进一步提高速度。

6.
整体效率

  • 通过上述方法,快读在处理大量数据时可以显著减少总的输入时间,提高程序的效率,尤其是在比赛中,输入输出的效率直接影响到整体的运行时间。

使用快读可以帮助选手在算法比赛中节省宝贵的时间,提升解题效率。

快读可以显著提高输入效率,尤其是在处理大量数据时。在比赛中,选择合适的快读方法有助于节省时间。使用时要注意数据的格式和边界条件。

大家好,我是木宛哥,今天和大家分享下——代码 CR 时针对恼人的空指针异常(
NullPointerException
)如何做到体系化去防控;

什么是空指针异常

从内存角度看,对象的实例化需要在堆内存中分配空间。如果一个对象没有被创建,那也就没有分配内存,当应用程序访问空对象时,实际上是访问一个“无效”的内存区域,从而导致系统抛出异常。

我们在 Java 编程时,空指针异常是一个常见的运行时错误,严重甚至会导致进程退出。所以这也是为什么我们要在 CR 时如此重视它的原因。

CR 我们要做什么

木宛哥认为 CR 应该重点关注三点:

  • 业务逻辑正确性
  • 代码的可读性和可维护性
  • 代码的健壮性和稳定性

OK,再回过头来看,针对空指针异常,在 CR 时,更多要从代码的健壮性和稳定性切入,可分为:

  • 防御性去杜绝空指针异常的出现(大多是访问了不存在的对象)
  • 对三方框架或者 JDK 认知不完善导致的潜在空指针异常发生(更多靠评审参与者经验分享)

防御性编程

防御性编程是非常有必要的,一方面可以提高系统稳定性和健壮性,另一方面可以形成比较好的代码规范;同时也是非常重要的思想,每个人都会如此去实践,在 CR 时针对空指针异常的防御是
common sense
;例如:

1.防御使用了未初始化的对象:

MyObject obj = null;
if (obj!= null){
    obj.someMethod();
}

2.防御使用了对象没有初始化的字段;

class MyClass {
   String name;
}
MyClass obj = new MyClass();
if (StringUtils.isNotBlank(obj.getName())){
    // do something
}

3.防御当调用方法返回 null 后,试图对返回的对象调用其方法或属性:

MyObject obj = getMyObject(); // 假设返回 null
if (obj != null) {
    obj.someMethod();
}

4.防御访问了集合中不存在的元素:

  • Map.get() 方法返回 null
  • Queue 的方法如 poll() 或 peek() 返回 null
Map<String ,String> dummyMap = new HashMap<>();
String value = dummyMap.get("key");
if (org.apache.commons.lang3.StringUtils.isNotBlank(value)){
    // do something
}

三方框架或 JDK 使用不当引发空指针异常提前排雷

这一类更多是三方框架或 JDK 的内部机制不清楚导致的踩坑,只有踩了这种类,

才会恍然大悟:“哦,原来这样啊,下回得注意了”;

所以针对这类问题,更多需要评审参与人的经验去发现,需要团队去共创,共建知识体系,例如:在团队空间维护“ TOP 100 踩坑记”等等;

在上篇文章《为什么建议使用枚举来替换布尔值》中,木宛哥提到过
Boolean

null
时产生的第三种结果,易造成
if
条件判断拆箱引发空指针问题,今天再继续分享其他:

1.三目运算符拆箱空指针问题

int var1 = 20;
Integer var2 = null;
boolean condition = true;
// 三目运算符拆箱问题,发生 NullPointerException
System.out.println(condition ? var2 : var1);

这里:
condition

true
,所以三目运算符选择了
var2
(即
null
)。即:
var2

Integer
类型)赋值给
num

Integer
类型)。理论上在这里应该是
num
被赋值为
null

但在 Java 中,三目运算符的返回类型需要通过类型来推导:

  • 如果
    var1

    int
    类型,而
    var2

    Integer
    类型,三目运算符会将它们的类型推导合并,令返回值为
    int
    类型。
  • 这意味着,如果
    condition

    true
    ,则会尝试将
    var2

    null
    )拆箱成
    int
    。由于
    null
    不能拆箱成
    int
    ,因此会抛出
    NullPointerException

这类典型的问题更多需要在 CR 时提前暴露出来,保证一致的参数类型来避免拆箱;

2.日志打印使用 fastjson 序列化时造成的空指针问题

大部分程序员编程开发习惯,喜欢打印参数到日志里,但有时候一个不起眼的
log.info
打印日志有可能导致接口异常;

如下打印日志,结果
fastjson
序列化异常,发生
NullPointerException

@Test
public void testJSONString() {
    Employee employee = new Employee("jack", 100);
    //fastjson 序列化异常,发生 NullPointerException
    LoggerUtil.info(logger,"{}",JSON.toJSONString(employee));
}

static class Employee {

    private EmployeeId employeeId;
    private String name;
    private Integer salary;

    public Employee(String name, Integer salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }

    public Integer getSalary() {
        return salary;
    }

    public String getEmployeeId() {
        return this.employeeId.getId();
    }
}

static class EmployeeId {
    private String id;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
}

原因在于
fastjson
使用
JSON.toJSONString(employee)
序列化成 JSON 时,底层实际通过解析
get
开头方法来识别属性,即:调用
get
方法获取属性的
value
值;上述代码:
employeeId

null
,但序列化时执行了
getEmployeeId
引发的空指针异常;

所以:特别是大家在实践 DDD 的时候,因为领域模型往往是充血模型,不仅有数据还包含了行为,对于行为可能习惯有
get
开头命名,要特别重视在打印领域模型时序列化问题;

3.对 Stream 流操作认知不完善导致的空指针异常

如果 Stream 流中存在空值,需要非常小心。

例如,如果第一个元素恰好为
null

findFirst()
将抛出
NullPointerException
。这是因为
findFirst()
返回一个
Optional
,而
Optional
不能包含空值。

Arrays.asList(null, 1, 2).stream().findFirst();//发生 NullPointerException

max()

min()

reduce()
,也表现出类似的行为。如果
null
是最终结果,则会抛出异常。

List<Integer> list = Arrays.asList(null, 1, 2);
var comparator = Comparator.<Integer>nullsLast(Comparator.naturalOrder());
System.out.println(list.stream().max(comparator));//发生 NullPointerException

再例如:我们在使用
Stream
流式编程时,如果流包含
null
,可以转换为
toList()

toSet()

然而,
toMap()
要注意, 不允许空值(允许空Key):

Employee employee1 = new Employee("Jack", 10000);
Employee employee2 = new Employee(null, 10000);
//toMap的Value不能为空,此处异常
Map<Integer, String> salaryMap = Arrays.asList(employee1, employee2)
    .stream()
    .collect(Collectors.toMap(Employee::getSalary, Employee::getName));

以及:
groupingBy()
不允许空 Key:

Employee employee1 = new Employee("Jack", 10000);
Employee employee2 = new Employee(null, 10000);

//groupingBy的Key不能为空,此处抛异常
Map<String, List<Employee>> result = Stream.of(employee1, employee2)
    .collect(Collectors.groupingBy(Employee::getName));

可见在流中使用了空对象存在许多陷阱;所以,在 CR 时,要重点关注 Stream 流的数据来源,避免在流中存在
null
,不确定的话建议用
filter(Objects::nonNull)
将它们过滤掉。

再谈空指针防控手段

上一章更多还是从防御空指针去解问题,但能保证每个人都是认知一样吗,同时在 CR 时也会有漏网之鱼;下面代码我想每个人都会这样去避免空指针,但难免在某个加班到凌晨的日子,脑袋一抽筋写反了:(

if("DEFAULT".equals(var)){
    //do something
}

所以,在这一章,木宛哥从数据来源切入,回答:
“能否数据天生就是存在非空的、方法天生就是不会返回 null”?

从程序角度来看,是合理的;许多变量永远不包含
null
,许多方法也永远不返回
null
。我们可以分别称它们为“非空变量”和“非空方法”(
NonNull
);

其他变量和方法在某些情况下可能会包含或返回
null
,它们称为“可空”(
Nullable
);

基于这个理论,在解空指针问题时,提供了另一种方式解法:

  • 数据来自三方系统,控制权不在我们,故:不可信任,需要做好防御编程;
  • 数据来自自身,控制权在我们,控制数据创建即非空,故:可信任;如下:

尽可能屏蔽 null 值

对输入值进行校验——在公共方法和构造函数中。需在每个
set
字段的入口处添加
Objects.requireNonNull()
调用。
requireNonNull()
方法会在其参数为
null
时抛出
NullPointerException

public class Employee {
    public Employee(String name, Integer salary) {
        this.name = Objects.requireNonNull(name);
        this.salary = Objects.requireNonNull(salary);
    }
}

这样做有助于在入口处屏蔽
null
值的写入

如果你的方法接受集合作为输入,也可以在方法入口遍历该集合以确保它不包含
null
值:

public void check(Collection<String> data) { 
    data.forEach(Objects::requireNonNull);
}

注:此处需要视具体集合的大小以及评估性能损耗;

同样的类型场景,不详细举例了:

  • 当你的类型是集合时,返回一个空容器,而不是返回
    null
    ,可以避免消费方出现空指针异常;
  • 使用枚举常量来替换
    Boolean
    来避免拆箱引入的空指针异常
  • 非法数据状态,直接短路抛出异常而不是返回
    null

善用静态分析工具来辅助

介绍两个重要的注解:
@Nullable

@NotNull
注解:

  • @Nullable
    注解意味着预期被注释的变量可能包含
    null
    ,或者被注释的方法可能返回
    null
  • @NotNull
    注释意味着预期的值绝不是
    null
    。并为静态分析提供了提示

这类注解,可以在静态分析工具实时分析潜在的异常;

interface Processor {
    @NotNull 
    String getNotNullValue();
    
    @Nullable
    String getNullable();
    
    public void process() {
        //此处警告:条件永远为假,不用多次一举
        if (getNotNullValue() == null) { 
            //do something
        } 
        //此处警告:trim() 调用可能导致 NullPointerException
        System.out.println(getNullable().trim()); 
    }
}

再谈使用 Optional 替代 null 的一些注意事项

为了避免使用
null
,一些开发者倾向于使用
Optional
类型。可以将
Optional
想象成一个盒子,它要么是空的,要么包含一个非
null
的值:

获取Optional对象有三种标准方式:

  • Optional.empty()
    —— 获取一个空的
    Optional
  • Optional.of(value)
    —— 获取一个非空的
    Optional
    ,如果值为
    null
    则抛出
    NullPointerException
  • Optional.ofNullable(value)
    —— 如果值为
    null
    则获取一个空的
    Optional
    ,否则获取一个包含值的非空
    Optional

使用
Optional
来预防空指针,大问题没有,
但有几个细节需要注意

1.勿滥用 ofNullable

一些开发者喜欢在所有地方使用
ofNullable()
,因为它被认为是更安全的,它从不抛出异常。但不能滥用,如果你已经知道你的值永远不会为
null
,最好使用
Optional.of()
。在这种情况下,如果你看到一个异常,你会立即知道错了并且修复;

2.Optional 造成的代码可读性降低

如下代码获取员工地址,虽然简洁,但可读性很差,对于嵌套特别深的情况下,我还是不建议使用
Opinional
,毕竟代码除了给自己看还得让别人也一眼明白意图

String employeeAddress = Optional.ofNullable(employee)
        .map(Employee::getAddress)
        .map(Address::getStreet)
        .map(Street::getNo)
        .map(No::getNumber).orElseThrow(() -> new IllegalArgumentException("非法参数"));

事后的异常监控

事前禁止写入
null
,事中防御性编程空指针异常,但真的高枕无忧了吗?

未必,事后所以建立一套好的异常告警机制是非常重要的;

我建议针对关键字:
NullPointerException
做单独的日志采集,同时配上相应的告警级别:理论上出现 1 次空指针异常就应该介入定位;

当然,特别是在发布周期内,如果
N
分钟内出现超过
M
次空指针异常那就肯定要快速定位和回滚了;

写在最后

欢迎关注我的公众号:编程启示录,第一时间获取最新消息;

微信 公众号
image image

实现目标
:能将Rust对象快速的映射到lua中使用,尽可能的简化使用。

功能目标


struct HcTestMacro
为例:

  1. 类型构建
    ,在lua调用
    local val = HcTestMacro.new()
    可构建
  2. 类型析构
    ,在lua调用
    HcTestMacro.del(val)
    可析建,仅限
    light use**rdata
  3. 字段的映射
    ,假设有字段
    hc
    ,我们需要能快速的进行字段的取值赋值
  • 取值

    val.hc
    或者
    val:get_hc()
    均可进行取值
  • 赋值

    val.hc = "hclua"
    或者
    val:set_hc("hclua")
    均可进行取值
  1. 类型方法
    ,注册类方法,比如额外的方法
    call1
    ,那我们就可以通过注册到lua虚拟机,由于lua虚拟机可能不会是全局唯一的,所以不好通过宏直接注册
// 直接注册函数注册
HcTestMacro::object_def(&mut lua, "ok", hclua::function1(HcTestMacro::ok));
// 闭包注册单参数
HcTestMacro::object_def(&mut lua, "call1", hclua::function1(|obj: &HcTestMacro| -> u32 {
    obj.field
}));
// 闭包注册双参数
HcTestMacro::object_def(&mut lua, "call2", hclua::function2(|obj: &mut HcTestMacro, val: u32| -> u32 {
    obj.field + val
}));
  1. 静态方法
    ,有些静态类方法,即不实际化对象进行注册可相当于模块
HcTestMacro::object_static_def(&mut lua, "sta_run", hclua::function0(|| -> String {
    "test".to_string()
}));

完整示列代码

use hclua_macro::ObjectMacro;

#[derive(ObjectMacro, Default)]
#[hclua_cfg(name = HcTest)]
#[hclua_cfg(light)]
struct HcTestMacro {
    #[hclua_field]
    field: u32,
    #[hclua_field]
    hc: String,
}

impl HcTestMacro {
    fn ok(&self) {
        println!("ok!!!!");
    }
}


fn main() {
    let mut lua = hclua::Lua::new();
    let mut test = HcTestMacro::default();
    HcTestMacro::register(&mut lua);
    // 直接注册函数注册
    HcTestMacro::object_def(&mut lua, "ok", hclua::function1(HcTestMacro::ok));
    // 闭包注册单参数
    HcTestMacro::object_def(&mut lua, "call1", hclua::function1(|obj: &HcTestMacro| -> u32 {
        obj.field
    }));
    // 闭包注册双参数
    HcTestMacro::object_def(&mut lua, "call2", hclua::function2(|obj: &mut HcTestMacro, val: u32| -> u32 {
        obj.field + val
    }));
    HcTestMacro::object_static_def(&mut lua, "sta_run", hclua::function0(|| -> String {
        "test".to_string()
    }));
    lua.openlibs();
    
    let val = "
        print(aaa);
        print(\"cccxxxxxxxxxxxxxxx\");
        print(type(HcTest));
        local v = HcTest.new();
        print(\"call ok\", v:ok())
        print(\"call1\", v:call1())
        print(\"call2\", v:call2(2))
        print(\"kkkk\", v.hc)
        v.hc = \"dddsss\";
        print(\"kkkk ok get_hc\", v:get_hc())
        v.hc = \"aa\";
        print(\"new kkkkk\", v.hc)
        v:set_hc(\"dddddd\");
        print(\"new kkkkk1\", v.hc)
        print(\"attemp\", v.hc1)
        print(\"vvvvv\", v:call1())
        print(\"static run\", HcTest.sta_run())
        HcTest.del(v);
    ";
    let _: Option<()> = lua.exec_string(val);
}

源码地址

hclua
Rust中的lua绑定。

功能实现剥析

通过derive宏进行函数注册:
#[derive(ObjectMacro, Default)]
通过attrib声明命名:
#[hclua_cfg(name = HcTest)]
,配置该类在lua
中的名字为
HcTest
,本质上在lua里注册全局的table,通过在该table下注册
HcTest { new = function(), del = function() }
通过attrib注册生命:
#[hclua_cfg(light)]
,表示该类型是
light userdata
即生命周期由Rust控制,默认为
userdata
即生命周期由Lua控制,通过
__gc
进行回收。
通过attrib声明字段:
#[hclua_field]
放到字段前面,即可以注册字段使用,在derive生成的时候判断是否有该字段,进行字段的映射。

derive宏实现

主要源码在
hclua-macro
实现, 完整代码可进行参考。

  1. 声明并解析ItemStruct
#[proc_macro_derive(ObjectMacro, attributes(hclua_field, hclua_cfg))]
pub fn object_macro_derive(input: TokenStream) -> TokenStream {
    let ItemStruct {
        ident,
        fields,
        attrs,
        ..
    } = parse_macro_input!(input);
  1. 解析Config,即判断类名及是否light
let config = config::Config::parse_from_attributes(ident.to_string(), &attrs[..]).unwrap();
  1. 解析字段并生成相应的函数
let functions: Vec<_> = fields
    .iter()
    .map(|field| {
        let field_ident = field.ident.clone().unwrap();
        if field.attrs.iter().any(|attr| attr.path().is_ident("hclua_field")) {
            let get_name = format_ident!("get_{}", field_ident);
            let set_name = format_ident!("set_{}", field_ident);
            let ty = field.ty.clone();
            quote! {
                fn #get_name(&mut self) -> &#ty {
                    &self.#field_ident
                }

                fn #set_name(&mut self, val: #ty) {
                    self.#field_ident = val;
                }
            }
        } else {
            quote! {}
        }
    })
    .collect();

let registers: Vec<_> = fields.iter().map(|field| {
    let field_ident = field.ident.clone().unwrap();
    if field.attrs.iter().any(|attr| attr.path().is_ident("hclua_field")) {
        let ty = field.ty.clone();
        let get_name = format_ident!("get_{}", field_ident);
        let set_name = format_ident!("set_{}", field_ident);
        quote!{
            hclua::LuaObject::add_object_method_get(lua, &stringify!(#field_ident), hclua::function1(|obj: &mut #ident| -> &#ty {
                &obj.#field_ident
            }));
            // ...
        }
    } else {
        quote!{}
    }
}).collect();

通过生成TokenStream数组,在最终的时候进行源码展开
#(#functions)*
即可以得到我们的TokenStream拼接的效果。

  1. 生成最终的代码
let name = config.name;
let is_light = config.light;
let gen = quote! {
    impl #ident {
        fn register_field(lua: &mut hclua::Lua) {
            #(#registers)*
        }

        fn register(lua: &mut hclua::Lua) {
            let mut obj = if #is_light {
                hclua::LuaObject::<#ident>::new_light(lua.state(), &#name)
            } else {
                hclua::LuaObject::<#ident>::new(lua.state(), &#name)
            };
            obj.create();

            Self::register_field(lua);
        }

        fn object_def<P>(lua: &mut hclua::Lua, name: &str, param: P)
        where
            P: hclua::LuaPush,
        {
            hclua::LuaObject::<#ident>::object_def(lua, name, param);
        }

        #(#functions)*
    }
    // ...
};
gen.into()

这样子我们通过宏就实现了我们快速的实现方案。

Field映射的实现

Lua对象映射中,
type(val)
为一个object变量,在这基础上进行访问的都将会触发元表的操作
metatable

Field的获取

我们访问任何对象如
val.hc

  1. 查找val中是否有hc的值,若存在直接返回
  2. 查找object中对应的元表
    lua_getmetatable
    若为meta
  3. 找到
    __index
    的key值,若不存在则返回空值
  4. 调用
    __index
    函数,此时调用该数第一个参数为
    val
    ,第二个参数为
    hc
  5. 此时有两种可能,一种是访问函数跳转6,一种是访问变量跳转7,
  6. 将直接取出meta["hc"]返回给lua,如果是值即为值,为函数则返回给lua的后续调用,通常的形式表达为
    val:hc()

    val.hc(val)
    实现调用,结束流程
  7. 因为变量是一个动态值,我们并未存在metatable中,所以需要额外的调用取出正确值,我们将取出的函数手动继续在调用
    lua_call(lua, 1, 1);
    即可以实现字段的返回

注:在变量中该值是否为字段处理过程会有相对的差别,又需要高效的进行验证,这里用的是全局的静态变量来存储是否为该类型的字段值。

lazy_static! {
    static ref FIELD_CHECK: RwLock<HashSet<(TypeId, &'static str)>> = RwLock::new(HashSet::new());
}

完整源码:

extern "C" fn index_metatable(lua: *mut sys::lua_State) -> libc::c_int {
    unsafe {
        if lua_gettop(lua) < 2 {
            let value = CString::new(format!("index field must use 2 top")).unwrap();
            return luaL_error(lua, value.as_ptr());
        }
    }
    if let Some(key) = String::lua_read_with_pop(lua, 2, 0) {
        let typeid = Self::get_metatable_real_key();
        unsafe {
            sys::lua_getglobal(lua, typeid.as_ptr());
            let is_field = LuaObject::is_field(&*key);
            let key = CString::new(key).unwrap();
            let t = lua_getfield(lua, -1, key.as_ptr());
            if !is_field {
                if t == sys::LUA_TFUNCTION {
                    return 1;
                } else {
                    return 1;
                }
            }
            lua_pushvalue(lua, 1);
            lua_call(lua, 1, 1);
            1
        }
    } else {
        0
    }
}

此时字段的获取已经完成了。

Field的设置

此时我们需要设置对象
val.hc = "hclua"

  1. 查找val中是否有hc的值,若有直接设置该值
  2. 查找object中对应的元表
    lua_getmetatable
    若为meta
  3. 找到
    __newindex
    的key值,若不存在则返回空值
  4. 调用
    __newindex
    函数,此时调用该数第一个参数为
    val
    ,第二个参数为
    hc
    ,第三个参数为字符串
    "hclua"
  5. 若此时判断第二个参数不是字段,则直接返回lua错误内容
  6. 此时我们会在第二个参数的key值后面添加
    __set
    即为
    hc__set
    ,我们查找meta["hc__set"] 若为空则返回失败,若为函数则转到7
  7. 我们将调用该函数,并将第一个参数
    val
    ,第三个参数
    hclua
    ,并进行函数调用
lua_pushvalue(lua, 1);
lua_pushvalue(lua, 3);
lua_call(lua, 2, 1);

此时字段的设置已经完成了。

小结

Lua的处理速度较慢,为了高性能,通常有许多函数会放到Rust层或者底层进行处理,此时有一个快速的映射就可以方便代码的快速使用复用,而通过derive宏,我们可以快速的构建出想要的功能。


title: Nuxt.js 应用中的 build:before 事件钩子详解
date: 2024/10/20
updated: 2024/10/20
author:
cmdragon

excerpt:
build:before 钩子在 Nuxt.js 中是一种有力的工具,使开发者能够在应用的构建流程开始之前进行自定义处理和配置。在处理动态需求和配置时,开发者可以充分利用这个钩子来增强应用的有效性和灵活性。

categories:

  • 前端开发

tags:

  • Nuxt
  • 构建
  • 钩子
  • 自定义
  • 配置
  • 环境
  • Webpack


image
image

扫描
二维码
关注或者微信搜一搜:
编程智域 前端至全栈交流与成长

build:before
钩子详解

build:before
是 Nuxt.js 的一个生命周期钩子,在 Nuxt 应用的打包构建器执行之前被调用。该钩子为开发者提供了一个在构建过程开始之前进行自定义配置和逻辑处理的机会。


目录

  1. 概述
  2. build:before 钩子的详细说明
  3. 具体使用示例
  4. 应用场景
  5. 注意事项
  6. 关键要点
  7. 总结


1. 概述

build:before
钩子提供了一种方法,让开发者能够在构建即将开始时修改配置或执行特定的前置逻辑。这对配置和文件准备工作尤其有用。

2. build:before 钩子的详细说明

2.1 钩子的定义与作用

  • 定义
    :
    build:before
    是 Nuxt.js 生命周期的一部分,允许开发者在打包构建器启动之前触发自定义逻辑。
  • 作用
    : 开发者可以在此时自定义构建前的操作,例如更新配置、设置环境变量等。

2.2 调用时机

  • 执行环境
    : 该钩子在 Nuxt 应用开始打包之前被触发,适合做一次性的预处理。
  • 挂载时机
    : 当 Nuxt 的构建过程启动之前,
    build:before
    钩子被调用。

2.3 返回值与异常处理

  • 返回值: 钩子没有要求返回值。
  • 异常处理: 应适当捕获和处理潜在的异常,以防止构建流程中断。

3. 具体使用示例

3.1 动态环境变量示例

下面是一个示例,展示如何在
build:before
钩子中动态设置环境变量:

// plugins/buildBeforePlugin.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('build:before', () => {
    process.env.CUSTOM_ENV_VARIABLE = 'some_value';
    console.log('Custom environment variable set:', process.env.CUSTOM_ENV_VARIABLE);
  });
});

在这个示例中,我们通过
build:before
钩子设置了一个自定义环境变量。

3.2 自定义 Webpack 配置示例

开发者也可以在构建之前修改 Webpack 配置:

// plugins/buildBeforePlugin.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hooks('build:before', (builder) => {
    const customWebpackConfig = {
      // 示例:增加某个插件
      plugins: [
        new SomeWebpackPlugin(),
      ],
    };

    // 合并自定义配置
    builder.extendWebpack((config) => {
      Object.assign(config, customWebpackConfig);
    });
  });
});

在这个示例中,我们在构建前自定义了 Webpack 配置,增加了一个插件。

4. 应用场景

  1. 配置修改
    : 在构建之前修改重要配置如环境变量或API端点。
  2. 动态构建
    : 根据特定条件动态生成配置,以适应不同的构建环境。
  3. 预处理
    : 进行文件的预处理或清理,确保构建环境的整洁。

5. 注意事项

  • 效率
    : 确保在钩子中执行的逻辑不会影响构建性能,尽量避免复杂的计算或大量的I/O操作。
  • 检查条件
    : 保证条件逻辑的清晰性,以免影响到构建过程。
  • 错误处理
    : 在钩子中如遇错误需要及时捕抓并处理,避免构建中断。

6. 关键要点

  • build:before
    钩子提供了构建过程之前自定义应用逻辑的机制。
  • 通过合理运用该钩子,可以增强应用的构建灵活性和可靠性。
  • 适当的错误处理和逻辑检查对构建成功至关重要。

7. 总结

build:before
钩子在 Nuxt.js 中是一种有力的工具,使开发者能够在应用的构建流程开始之前进行自定义处理和配置。在处理动态需求和配置时,开发者可以充分利用这个钩子来增强应用的有效性和灵活性。

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长
,阅读完整的文章:
Nuxt.js 应用中的 build:before 事件钩子详解 | cmdragon's Blog

往期文章归档: