Egg考古系列-EggCore的生命周期

news2024/11/25 8:18:40
alt

关于EGG

egg框架的第一个版本还是2017-03-21,距今已有7年了。虽然最近几年没有什么更新,但它在国内的使用还是挺多的,mvc的分层模式也很受大家喜欢。虽然声称是面向企业级、中大型项目场景的框架,但这种约定式在大型项目中其实也很容易导致依赖混乱,service层管理不好容易不断膨胀,变成一个大文件,同时service层内部也会存在多个交叉调用的情况。不过这不是重点。我们这次来看看egg-core。

Egg-Core

Egg-core 是 Egg.js 框架的核心模块,用于管理整个框架的底层机制和核心功能。它负责框架的启动、插件加载、应用生命周期管理等关键任务,是 Egg.js 的基础组件之一。今天先介绍一下egg的生命周期的实现。

egg的生命周期如下:

export interface ILifecycleBoot {
  /**
   * Ready to call configDidLoad,
   * Config, plugin files are referred,
   * this is the last chance to modify the config.
   */
  configWillLoad?(): void;

  /**
   * Config, plugin files have loaded
   */
  configDidLoad?(): void;

  /**
   * All files have loaded, start plugin here
   */
  didLoad?(): Promise<void>;

  /**
   * All plugins have started, can do some thing before app ready
   */
  willReady?(): Promise<void>;

  /**
   * Worker is ready, can do some things,
   * don't need to block the app boot
   */
  didReady?(err?: Error): Promise<void>;

  /**
   * Server is listening
   */
  serverDidReady?(): Promise<void>;

  /**
   * Do some thing before app close
   */
  beforeClose?(): Promise<void>;
}

其实现原理主要依赖于这两个包get-readyready-callback。其中ready-callback是对get-ready的二次封装。我们先来看看这两个包的作用。

什么是get-ready

get-ready是用于一次性就绪事件,这其实在我们实际的项目中也是经常会需要用到。例如项目启动前需要先请求几个接口,这些数据都准备好之后,才启动。这种场景就很合适了。

其实现的主要代码如下:

export class Ready {
  #isReady: boolean;
  #readyCallbacks: CallbackFunction[];
  #readyArg?: Error = undefined;

  constructor() {
    this.#isReady = false;
    this.#readyCallbacks = [];
  }

  ready(flagOrFunction?: ReadyFunctionArg) {
    // 注册回调
    if (flagOrFunction === undefined || typeof flagOrFunction === 'function') {
      return this.#register(flagOrFunction);
    }
    // emit callbacks
    this.#emit(flagOrFunction);
  }

  /**
   * 注册回调函数
   */
  #register(func?: CallbackFunction) {
    // support `this.ready().then(onready);` and `await this.ready()`;
    if (!func) {
      return new Promise<void>((resolve, reject) => {
        function func(err?: Error) {
          if (err) {
            reject(err);
          } else {
            resolve();
          }
        }
        if (this.#isReady) {
          return func(this.#readyArg);
        }
        this.#readyCallbacks.push(func);
      });
    }

    // this.ready(fn)
    if (this.#isReady) {
      func(this.#readyArg);
    } else {
      this.#readyCallbacks.push(func);
    }
  }

  /**
  * 调用已注册的回调,并清理回调堆栈。
  *如果标志不是假的,它将被标记为就绪。然后,回调将在注册时立即调用。
  * @param {Boolean|Error} 标志 - 设置标志是否已准备就绪。如果标志是错误,它也已准备就绪,但回调将使用参数“错误”调用
*/
  #emit(flag: boolean | Error) {
    // this.ready(true);
    // this.ready(false);
    // this.ready(err);
    this.#isReady = flag !== false;
    this.#readyArg = flag instanceof Error ? flag : undefined;
    // this.ready(true)
    if (this.#isReady) {
      this.#readyCallbacks
        .splice(0, Infinity)
        .forEach(callback => process.nextTick(() => callback(this.#readyArg)));
    }
  }

  /**
   * @param {Object} obj - an object that be mixed
   */
  static mixin(obj?: any) {
    if (!obj) return;
    const ready = new Ready();
    // delegate method
    obj.ready = (flagOrFunction: any) => ready.ready(flagOrFunction);
  }
}

这段代码定义了一个名为 Ready 的类,它用于管理异步操作的就绪状态。

  1. Ready 包含三个私有成员变量:

    • #isReady:一个布尔值,表示是否已经就绪。
    • #readyCallbacks:一个回调函数数组,用于存储需要在就绪时调用的函数。
    • #readyArg:一个可选的 Error 对象,用于传递给回调函数的错误信息。
  2. 构造函数 constructor 初始化这些私有成员变量:

    • this.#isReady 被设置为 false,表示初始状态为未就绪。
    • this.#readyCallbacks 被初始化为空数组。
  3. ready 方法是类的公共接口,用于注册回调函数或触发回调:

    • 如果传入的参数 flagOrFunctionundefined 或者是一个函数,那么会调用 #register 方法注册回调函数。
    • 如果 flagOrFunction 是一个布尔值或 Error 对象,那么会调用 #emit 方法触发回调。
  4. #register 方法用于注册回调函数:

    • 如果没有传入回调函数( func),则返回一个 Promise 对象,以便可以在回调被触发时解析或拒绝。
    • 如果已经就绪( this.#isReadytrue),则立即调用回调函数。
    • 如果尚未就绪,将回调函数添加到 #readyCallbacks 数组中。
  5. #emit 方法用于触发所有注册的回调函数,并清理回调栈:

    • 如果传入的 flag 不是 false,则将 #isReady 设置为 true
    • 如果 flag 是一个 Error 对象,那么 #readyArg 被设置为这个错误对象。
    • 如果已经就绪,那么遍历 #readyCallbacks 数组,使用 process.nextTick 异步调用每个回调函数,并传递 #readyArg 作为参数。
  6. mixin 方法是一个静态方法,用于将 Ready 类的功能混入到其他对象中:

    • 如果传入了对象 obj,则在该对象上添加一个 ready 方法,该方法会委托给 Ready 类的实例。

其使用方法如下:

import { Ready } from 'get-ready';

const obj = new Ready();

// register a callback
obj.ready(() => console.log('ready'));

// mark ready
obj.ready(true);

比较巧妙的是,上面的ready支持无参,无参的时候register会自动注册一个包含promise的方法。这样,如果只想等待ready后再执行后续代码,那可以这样子使用。

await obj.ready();
// 执行其它业务

在效果上,其实跟vue里面的$nextTick有点类似,既可以传回调,也可以提供一个无参的调用。

在设计上,它坚持了开闭原则、最小知识原则,暴露了mixin去提供拓展,二次包装的可能,同时对修改关闭,只有一个ready方法去控制注册和触发,让使用者无需关心过多的接口,无需考虑过多的使用依赖问题。

在使用上,我们可以在需要的时候注册回调函数,并在某个时刻标记为就绪,触发所有回调函数。这在处理异步初始化或等待多个异步操作完成时非常有用。

什么是ready-callback

我们再来看看ready-callback,其内部实现引用了get-ready模块,说明其是对get-ready的二次封装。

import EventEmitter from 'node:events';
import { debuglog } from 'node:util';
import { randomUUID } from 'node:crypto';
import once from 'once';
import { Ready as ReadyObject, type ReadyFunctionArg } from 'get-ready';

const debug = debuglog('ready-callback');

const defaults: ReadyCallbackOption = {
  timeout: 10000,
  isWeakDep: false,
};
class Ready extends EventEmitter {
  isError = false;
  cache: ReadyCallbackCache = new Map();

  opt: ReadyOption;
  obj: any;

  ready: (flagOrFunction?: ReadyFunctionArg) => void;

  constructor(opt: ReadyOption = {}) {
    super();
    ReadyObject.mixin(this);

    this.opt = opt;

    if (!this.opt.lazyStart) {
      this.start();
    }
  }

  start() {
    setImmediate(() => {
      // 当没有注册的准备回调时,直接触发回调
      if (this.cache.size === 0) {
        debug('Fire callback directly');
        this.ready(true);
      }
    });
  }

  /**
   * Mix `ready` and `readyCallback` to `obj`
   * @function Ready#mixin
   * @param  {Object} obj - The mixed object
   * @return {Ready} this
   */
  mixin(obj?: any) {
    // only mixin once
    if (!obj || this.obj) return null;

    // delegate API to object
    obj.ready = this.ready.bind(this);
    obj.readyCallback = this.readyCallback.bind(this);

    // only ready once with error
    this.once('error', err => obj.ready(err));

    // delegate events
    if (obj.emit) {
      this.on('ready_timeout', obj.emit.bind(obj, 'ready_timeout'));
      this.on('ready_stat', obj.emit.bind(obj, 'ready_stat'));
      this.on('error', obj.emit.bind(obj, 'error'));
    }
    this.obj = obj;

    return this;
  }

  readyCallback(name: string, opt: ReadyCallbackOption = {}) {
    opt = Object.assign({}, defaults, this.opt, opt);
    const cacheKey = randomUUID();
    opt.name = name || cacheKey;
    const timer = setTimeout(() => this.emit('ready_timeout', opt.name), opt.timeout);
    const cb = once((err?: any) => {
      if (err != null && !(err instanceof Error)) {
        err = new Error(err);
      }
      clearTimeout(timer);
      // won't continue to fire after it's error
      if (this.isError === truereturn;
      // fire callback after all register
      setImmediate(() => this.readyDone(cacheKey, opt, err));
    }) as unknown as ReadyCallbackFn;
    debug('[%s] Register task id `%s` with %j', cacheKey, opt.name, opt);
    cb.id = opt.name;
    this.cache.set(cacheKey, cb);
    return cb;
  }

  readyDone(id: string, opt: ReadyCallbackOption, err?: Error) {
    if (err != null && !opt.isWeakDep) {
      this.isError = true;
      debug('[%s] Throw error task id `%s`, error %s', id, opt.name, err);
      return this.emit('error', err);
    }

    debug('[%s] End task id `%s`, error %s', id, opt.name, err);
    this.cache.delete(id);

    this.emit('ready_stat', {
      id: opt.name,
      remain: getRemain(this.cache),
    });

    if (this.cache.size === 0) {
      debug('[%s] Fire callback async', id);
      this.ready(true);
    }
    return this;
  }
}

function getRemain(map: ReadyCallbackCache) {
  const names: string[] = [];
  for (const cb of map.values()) {
    names.push(cb.id);
  }
  return names;
}

export { Ready };

export default function(opt: ReadyOption = {}) {
  return new Ready(opt);
}

这段代码也定义了一个名为Ready的类,该类继承自EventEmitter类。Ready类用于管理和执行一组回调函数,这些回调函数在某个条件满足时被调用。它的构造函数接受一个opt参数,用于配置类的实例。

  • 再构建函数里mixin了 get-ready类,使其具备了 get-ready的ready方法。可以注册回调和触发回调。
  • start方法用于开始处理回调队列。
  • mixin方法允许将 readyreadyCallback方法添加到另一个对象。
  • readyCallback方法用于登记一个调用标识,其内部会返回一个函数,函数被调用会触发 readyDone方法。
  • readyDone方法用于标记一个调用标识已完成,并触发相应的事件。
  • getRemain函数用于获取尚未完成的回调函数的ID列表。

start方法和readyDone方法都会去检查是否所有注册的调用标识都已完成,完成则触发get-ready.ready(true),以便执行自己注册的回调。

使用案例如下:

var koa = require('koa');
var ready = require('ready-callback')();
var app = koa();
ready.mixin(app);

const endA = app.readyCallback('a');
const endB = app.readyCallback('b');
const endC = app.readyCallback('c');
const endD = app.readyCallback('d');
setTimeout(endA, 1);
setTimeout(endB, 80);
setTimeout(endC, 10);
setTimeout(endD, 50);

// callback will be fired after all service launched
app.ready(function() {
  app.listen();
});

在使用上,这个类允许用户注册回调函数,并在所有回调都完成或超时后触发一个最终的回调。它还提供了错误处理和事件发射机制,以便用户可以响应不同的状态变化。

get-ready和ready-callback的区别

了解了两个模块,在egg-core的Lifecycle里两者都有使用,其有三个内置成员变量,如下:

  • readyObject: get-ready实例,私有变量
  • loadReady: ready-callback实例,共有变量
  • bootReady: ready-callback实例,共有变量

那他们有什么区别呢?两者的流程示意图如下:

alt
alt

ready-callback是对get-ready的二次包装,增加了超时、延迟启动的特性。同时增加了一个计数器的功能,可以通过程序代码,在自己的业务逻辑里登记标识,消费标识,最终在所有都准备好之后,执行一些回调。

egg-core的Lifecycle

那我们接下来看看Lifecycle,从字面上就可以知道它是管理生命周期的,在app.js文件加载的时候,就会先调用这个方法把它关联进去this.lifecycle.addBootHook(bootHook);,egg自身是支持框架拓展和第三方拓展的,所以可能会有多个启动钩子被注册进去,Lifecycle都统一存储到了私有变量this.#bootHooks上。添加完后,则调用this.lifecycle.init();去初始化实例bootHook对象,并存储到this.#boots数组上。

Lifecycle统一封装了生命周期函数的调用,当要调用具体的钩子的时候则遍历上面的this.#boots

for (const boot of this.#boots) {
      if (typeof boot.configDidLoad === 'function') {
        boot.configDidLoad();
      }
      // function boot hook register after configDidLoad trigger
      if (typeof boot.beforeClose === 'function') {
        const beforeClose = boot.beforeClose.bind(boot);
        this.registerBeforeClose(beforeClose);
      }
    }

在Lifecycle里,生命周期函数的触发都被命名为trigger的触发函数里。

triggerConfigWillLoad() {
    debug('trigger configWillLoad start');
    for (const boot of this.#boots) {
      if (typeof boot.configWillLoad === 'function') {
        boot.configWillLoad();
      }
    }
    debug('trigger configWillLoad end');
    this.triggerConfigDidLoad();
  }

  triggerConfigDidLoad() {
    debug('trigger configDidLoad start');
    for (const boot of this.#boots) {
      if (typeof boot.configDidLoad === 'function') {
        boot.configDidLoad();
      }
      // function boot hook register after configDidLoad trigger
      if (typeof boot.beforeClose === 'function') {
        const beforeClose = boot.beforeClose.bind(boot);
        this.registerBeforeClose(beforeClose);
      }
    }
    debug('trigger configDidLoad end');
    this.triggerDidLoad();
  }
  
  triggerServerDidReady() {
    debug('trigger serverDidReady start');
    return (async () => {
      for (const boot of this.#boots) {
        if (typeof boot.serverDidReady !== 'function') {
          continue;
        }
        try {
          await boot.serverDidReady();
        } catch (err) {
          this.emit('error', err);
        }
      }
      debug('trigger serverDidReady end');
    })();
  }
  triggerDidLoad() {
    debug('trigger didLoad start');
    debug('loadReady start');
    this.loadReady.start();
    for (const boot of this.#boots) {
      if (typeof boot.didLoad === 'function') {
        const didLoad = boot.didLoad.bind(boot);
        this.#registerReadyCallback({
          scope: didLoad,
          ready: this.loadReady,
          timingKeyPrefix: 'Did Load',
          scopeFullName: boot.fullPath + ':didLoad',
        });
      }
    }
  }
  triggerWillReady() {
    debug('trigger willReady start');
    debug('bootReady start');
    this.bootReady.start();
    for (const boot of this.#boots) {
      if (typeof boot.willReady === 'function') {
        const willReady = boot.willReady.bind(boot);
        this.#registerReadyCallback({
          scope: willReady,
          ready: this.bootReady,
          timingKeyPrefix: 'Will Ready',
          scopeFullName: boot.fullPath + ':willReady',
        });
      }
    }
  }

  triggerDidReady(err?: Error) {
    debug('trigger didReady start');
    return (async () => {
      for (const boot of this.#boots) {
        if (typeof boot.didReady === 'function') {
          try {
            await boot.didReady(err);
          } catch (e) {
            this.emit('error', e);
          }
        }
      }
      debug('trigger didReady end');
    })();
  }

这里上面就涉及了5个生命周期函数:

  • 配置文件即将加载,这是最后动态修改配置的时机(configWillLoad);
  • 配置文件加载完成(configDidLoad);
  • 文件加载完成(didLoad);
  • 应用启动完成(serverDidReady);
  • 插件启动完毕(willReady);
  • worker 准备就绪(didReady);

那这些函数是怎么流转起来的呢?这就需要用到上面的2个模块了。主要涉及3个对象。其中1个get-ready实例和2个ready-callback实例。

在Lifecycle里,get-ready实例readyObject,它在构造函数里实例化

this.#readyObject = new ReadyObject();

并暴露了一个ready方法

ready(arg?: ReadyFunctionArg) {
    return this.#readyObject.ready(arg);
  }

这个方法会通过EggCore的ready暴露出去给外部调用。官方的说明是注册一个回调函数,该函数将在应用程序准备就绪时调用。例如:

import { EggCore as Application } from '@eggjs/core';

const app = new Application({
  baseDir: '/path/to/app'
});
app.ready(() => {
  app.listen(3000);
});

那它什么时候会被置为true呢?bootReady完成的时候。后面会串起来讲一下。

还有两位两个loadReady和bootReady,是ready-callback实例,也是在构建函数里实例化的。

this.loadReady = new Ready({ timeout: this.readyTimeout, lazyStart: true });

this.bootReady = new Ready({ timeout: this.readyTimeout, lazyStart: true });

从这里可以看到,其设置了超时和lazyStart,也就是要手动调用start的。

其暴露了两个方法,用于调用readyCallback设置计数标识,以便第三方插件去调用。

legacyReadyCallback(name: string, opt?: object) {
    const timingKeyPrefix = 'readyCallback';
    const timing = this.timing;
    const cb = this.loadReady.readyCallback(name, opt);
    const timingKey = `${timingKeyPrefix} in ` + utils.getResolvedFilename(name, this.app.baseDir);
    this.timing.start(timingKey);
    debug('register legacyReadyCallback');
    return function legacyReadyCallback(...args: any[]) {
      timing.end(timingKey);
      debug('end legacyReadyCallback');
      cb(...args);
    };
  }
  
  registerReadyCallback(args: {
    scope: Fun;
    ready: Ready;
    timingKeyPrefix: string;
    scopeFullName?: string;
  }) {
    const { scope, ready, timingKeyPrefix, scopeFullName } = args;
    if (typeof scope !== 'function') {
      throw new Error('boot only support function');
    }

    // get filename from stack if scopeFullName is undefined
    const name = scopeFullName || utils.getCalleeFromStack(true, 4);
    const timingKey = `${timingKeyPrefix} in ` + utils.getResolvedFilename(name, this.app.baseDir);

    this.timing.start(timingKey);

    debug('[registerReadyCallback] start name: %o', name);
    const done = ready.readyCallback(name);

    // ensure scope executes after load completed
    process.nextTick(() => {
      utils.callFn(scope).then(() => {
        debug('[registerReadyCallback] end name: %o', name);
        done();
        this.timing.end(timingKey);
      }, (err: Error) => {
        done(err);
        this.timing.end(timingKey);
      });
    });
  }

第三方插件通过egg的api去调用上面的函数,就可以去声明计数标识。 this.loadReady会在didLoad钩子里去调用this.loadReady.start()方法启动,启动之后会为this.#boots实例调用registerReadyCallback,增加计数标识。

triggerDidLoad() {
    debug('trigger didLoad start');
    debug('loadReady start');
    this.loadReady.start();
    for (const boot of this.#boots) {
      if (typeof boot.didLoad === 'function') {
        const didLoad = boot.didLoad.bind(boot);
        this.#registerReadyCallback({
          scope: didLoad,
          ready: this.loadReady,
          timingKeyPrefix: 'Did Load',
          scopeFullName: boot.fullPath + ':didLoad',
        });
      }
    }
  }

同样的this.bootReady则是在triggerWillReady去启动和为this.#boots实例调用registerReadyCallback,增加计数标识。

triggerWillReady() {
    debug('trigger willReady start');
    debug('bootReady start');
    this.bootReady.start();
    for (const boot of this.#boots) {
      if (typeof boot.willReady === 'function') {
        const willReady = boot.willReady.bind(boot);
        this.#registerReadyCallback({
          scope: willReady,
          ready: this.bootReady,
          timingKeyPrefix: 'Will Ready',
          scopeFullName: boot.fullPath + ':willReady',
        });
      }
    }
  }

不同的是,它的调用时机,它是在this.loadReady完成之后再调用的。

this.loadReady.ready((err?: Error) => {
      debug('loadReady end, err: %o', err);
      debug('trigger didLoad end');
      if (err) {
        this.ready(err);
      } else {
        this.triggerWillReady();
      }
    });

那readyObject呢?它在什么时候会被标识为完成?只需要在this.bootReady.ready之后即可。

this.bootReady.ready((err?: Error) => {
      debug('bootReady end, err: %o', err);
      debug('trigger willReady end');
      this.ready(err || true);
    });

我们再来整理一下对应的调用链路:

  • 线路1: this.loadCustomApp -> this.lifecycle.addBootHook() -> this.lifecycle.init() -> triggerConfigWillLoad -> configWillLoad -> configDidLoad -> this.loadReady.start -> this.loadReady.readyCallback

  • 线路2: this.loadReady.ready(true) -> didLoad -> this.bootReady.start -> this.loadReady.readyCallback

  • 线路3: this.bootReady.ready(true) -> willReady -> this.ready -> didReady -> 初始化完成 -> serverDidReady

每个线路的衔接就是通过ready-callback来完成的。其中加粗部分表示生命周期函数的调用。

egg的生命周期除了应用自身使用之外,还要给它的插件和框架开发使用的。它在设计上的巧妙之处在于引入Boot类来解耦跟插件和框架的藕合。允许开发者将应用程序的初始化、配置加载、插件启动、就绪和关闭等逻辑分散到不同的模块中。这种设计不仅使代码更加清晰和易于维护,而且便于扩展,因为新的Boot类可以轻松地添加到生命周期中。

而在事件衔接方面,则利用了计数标识的增加,满足条件后触发回调的方式,通过ready-callback模块衔接不同的事件,即便于开发者定义一系列需要完成的任务(通过readyCallback方法),并在所有任务都完成后触发一个就绪事件。它还支持设置超时时间,以确保在任务长时间无法完成时能够触发相应的处理逻辑。

总结

Egg虽然发布很多年了,但其内部还是有很多值得我们学习的地方。通过本文,我们了解了两个优秀的模块:

  • get-ready:可以用于简单的初始化管理
  • ready-callback:适合用于多状态、多插件的回调执行

另外我们也了解了egg生命周期的实现原理,其内部不同周期的串联逻辑。我觉得可以借鉴的是其解耦流程和插件的这种设计模式。希望对你有所帮助。

本文由 mdnice 多平台发布

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

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

相关文章

高校学科竞赛管理:SpringBoot实现的高效策略

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

【M2-Mixer】核心方法解读

abstract&#xff1a; 在本文中&#xff0c;我们提出了M2-Mixer&#xff0c;这是一种基于MLPMixer的结构&#xff0c;具有多头损失&#xff0c;用于多模态分类。它比基于卷积、循环或神经结构搜索的基线模型具有更好的性能&#xff0c;其主要优势是概念和计算简单。所提出的多…

【电子电力】LCL滤波器设计,包括电流控制调谐

摘要 LCL 滤波器是电力电子领域中广泛应用于并网逆变器的滤波器之一&#xff0c;其主要功能是减少高频开关的谐波&#xff0c;确保输出电流的质量。本文设计并实现了基于 MATLAB 的 LCL 滤波器模型&#xff0c;结合电流控制器和调谐技术&#xff0c;验证了其在谐波抑制方面的效…

从RNN讲起(RNN、LSTM、GRU、BiGRU)——序列数据处理网络

文章目录 RNN&#xff08;Recurrent Neural Network&#xff0c;循环神经网络&#xff09;1. 什么是RNN&#xff1f;2. 经典RNN的结构3. RNN的主要特点4. RNN存在问题——长期依赖&#xff08;Long-TermDependencies&#xff09;问题 LSTM&#xff08;Long Short-Term Memory&a…

PostgreSQL学习笔记七:常规SQL操作

PostgreSQL 支持标准的 SQL 语句&#xff0c;同时也扩展了一些特有的功能。以下是一些常规的 SQL 语句示例&#xff0c;这些示例涵盖了数据定义、数据操作和数据查询的基本操作&#xff1a; 数据定义语言 (DDL 创建数据库&#xff1a; CREATE DATABASE mydatabase;创建表&#…

stm32单片机个人学习笔记9(TIM输入捕获)

前言 本篇文章属于stm32单片机&#xff08;以下简称单片机&#xff09;的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 STM32入门教程-2023版 细…

AWD入门

一、简介 AWD(Attack With Defense&#xff0c;攻防兼备)模式。你需要在一场比赛里要扮演攻击方和防守方&#xff0c;攻者得分&#xff0c;失守者会被扣分。也就是说攻击别人的靶机可以获取 Flag 分数时&#xff0c;别人会被扣分&#xff0c;同时你也要保护自己的主机不被别人…

Docker 教程四 (Docker 镜像加速)

Docker 镜像加速 国内从 DockerHub 拉取镜像有时会遇到困难&#xff0c;此时可以配置镜像加速器。 目前国内 Docker 镜像源出现了一些问题&#xff0c;基本不能用了&#xff0c;后期能用我再更新下。* Docker 官方和国内很多云服务商都提供了国内加速器服务&#xff0c;例如…

Python网络爬虫入门指南

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

MPA-SVM多变量回归预测|海洋捕食者优化算法-支持向量机|Matalb

目录 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 亮点与优势&#xff1a; 二、实际运行效果&#xff1a; 三、算法介绍&#xff1a; 四、完整程序下载&#xff1a; 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 本代码基于Matlab平台编译&am…

2024年网络安全进阶学习路径-2024年进阶学习指南

&#x1f91f; 基于入门网络安全/黑客打造的&#xff1a;&#x1f449;黑客&网络安全入门&进阶学习资源包 前言 什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、…

KDD 2024论文分享┆用于序列推荐的数据集再生

论文简介 本推文介绍了2024 KDD的最佳学生论文《Dataset Regeneration for Sequential Recommendation》。该论文提出了一种基于数据中心化范式的新框架&#xff0c;称为DR4SR&#xff0c;该框架通过模型无关的数据再生机制&#xff0c;能够生成具有出色跨架构泛化能力的理想训…

git(版本回退,分支管理,vscode集成git)

一、安装与简单命令 1.官网 https://git-scm.com/downloads 2.查看版本号git --version 3.设置用户签名&#xff08;用户名和邮箱&#xff09; 用来标识用户&#xff0c;以区分不同的开发人员 git config --global user.name "Your Name" git config --global u…

2024年最新算法:青蒿素优化算法(Artemisinin Optimization Algorithm, AOA)原理介绍

青蒿素优化算法&#xff08;Artemisinin Optimization Algorithm, AOA&#xff09;是2024年提出的一种受青蒿素抗疟疾特性启发的元启发式优化算法。青蒿素是一种从中草药青蒿中提取的化合物&#xff0c;因其在治疗疟疾方面的显著效果而闻名。AOA算法的设计者将青蒿素的这一特性…

【机器学习】深入浅出讲解贝叶斯分类算法

0. 前言 1.贝叶斯分类器介绍 贝叶斯分类是一类分类算法的总称&#xff0c;这类算法均以贝叶斯定理为基础&#xff0c;故统称为贝叶斯分类。而朴素贝叶斯&#xff08;Naive Bayes&#xff09;分类是贝叶斯分类中最简单&#xff0c;也是常见的一种分类方法。 一些很常见的分类…

动态规划最大子段和讲解和【题解】——最大子段和

动态规划最大子段和讲解和【题解】——最大子段和 1.详细讲解最大子段和题目描述输入格式输出格式输入输出样例输入 #1输出 #1 提示样例 1 解释数据规模与约定 1.1.思路解析1.2.AC代码 2.优化3.别言 1.详细讲解 最大子段和 题目描述 给出一个长度为 n n n 的序列 a a a&am…

cursor: mutex X 等待事件分析

背景&#xff1a; v$session中同一个sql语句bhaku1zp2w5v7大量等待cursor: mutex X &#xff0c;且等待事件较长。 分析&#xff1a; 什么是cursor: mutex X&#xff1f; 任何操作或访问游标的操作都可能需要等待访问共享池中支持游标的结构。在极端争用的情况下&#xff0c…

MySQL 【数字】函数大全(一)

ABSCEILCEILINGCONVDIVFLOORCREATESTLEAST 1、ABS ABS(number) &#xff1a;返回指定数字的绝对值 如果参数 number 为字符串&#xff0c;ABS() 将按照如下规则尝试转为数字&#xff1a; 如果以数字开头&#xff0c;则将开头的数字部分转为数字。如果不能转为数字&#xff0c;…

使用Go语言的gorm框架查询数据库并分页导出到Excel实例

文章目录 基本配置配置文件管理命令行工具: Cobra快速入门基本用法 生成mock数据SQL准备gorm自动生成结构体代码生成mock数据 查询数据导出Excel使用 excelize实现思路完整代码参考 入口文件效果演示分页导出多个Excel文件合并为一个完整的Excel文件 完整代码 基本配置 配置文…

Vue环境安装以及配置

这里写目录标题 前言一、前置要求1.安装Node.js2. 安装VScode 二、创建全局安装目录和缓存日志目录三、配置环境变量四、权限五、配置镜像六、vscode插件1. Vue-Offical2. Vue 3 Snippets3. Path Intellisense4. Auto Import5. Auto Close Tag6. Auto Rename Tag7.GitLens总结 …