2024年6月

我们相信提高开发和团队协作的生产力可以帮助您产生更好的软件解决方案。这就是为什么 Visual Studio 版本控制团队发布了新特性,简化了内部循环和代码审查体验。您将获得 AI 支持编写提交消息、拉取请求描述、提交注释以及更好的拉取请求创建体验。下载最新的 Visual Studio 版本来试用一下,让我们知道您的想法!

自己写的越来越少,使用 GitHub Copilot 编写更多代码

从 Visual Studio 17.10 GA 版本开始,为了尝试生成拉请求描述和提交注释,您需要有一个激活的 GitHub Copilot 订阅,安装 GitHub Copilot 组件,并在 GitHub Copilot 菜单中启用 Git 预览功能。

更新生成的 Git 提交消息

我们听取了您的反馈,并缩短了我们生成的Git Commit特性的输出。请您在开发者社区中分享关于此更改的反馈。

使用生成的拉取请求描述改进代码审查

利用 GitHub Copilot,您现在可以为您的拉取请求生成第一稿描述。您将获得帮助,为同事提供重要的上下文以供他们审查,并获得额外的好处,确保您在拉取请求中包含了所有相关的更改。

在“Create a Pull Request”窗口中选择“Add AI Generated Description”闪光笔图标来查看您的描述。

阐明提交历史

我们在 Commit Details 窗口中添加了一个 GitHub Copilot 驱动的解释功能,使每次提交的内容更容易理解。您将获得与代码并列的更改摘要,突出显示关键差异及其背后的逻辑依据。

双击任何提交,打开 Git Repository 窗口中的 Commit Details 窗格。然后,选择“Explain”图标以获得更改的摘要。

在 Visual Studio 中使用工作项链接创建拉取请求

我们对预览版“Create a Pull Request”流程和后续更新的热情接待感到非常兴奋。这是现在 Visual Studio 的默认体验,包括来自 Azure DevOps 的头号请求,将工作项链接到拉取请求。现在,您可以使用 Related Work Item 部分查看您在描述中引用的任何项目,并将它们链接到 Azure DevOps 上的拉取请求。

帮助我们使 Visual Studio 版本控制工具变得更好

人工智能支持可以使繁琐的版本控制文档更容易处理。此外,更流畅的拉请求体验有助于您的合作蓬勃发展!尝试 Visual Studio 17.10 GA 中的最新功能,并在以下调查中分享您的反馈:

- AI 提交消息

- AI 拉取请求描述

- AI 注释提交

- 创建拉取请求改进

我们感谢您花时间报告问题/建议,并希望您在使用 Visual Studio 时继续给我们反馈,告诉我们您喜欢什么以及我们可以改进什么。您的反馈对于帮助我们使 Visual Studio 成为最好的工具至关重要!您可以通过开发者社区与我们分享反馈,通过发送反馈来报告问题或分享您的建议,推动对新功能或现有功能的改进。

通过在 YouTube, Twitter, LinkedIn, Twitch 和 Microsoft Learn 上关注我们与 Visual Studio 团队保持联系。

原文链接:https://devblogs.microsoft.com/visualstudio/catch-up-on-the-latest-git-tooling-features-in-17-10-ga/

一个 lfu(least frequently used/最不经常使用页置换算法 ) 缓存的实现,
其核心思想是淘汰一段时间内被访问次数最少的数据项
。与LRU(最近最少使用)算法不同,LFU更侧重于数据的访问频率而非访问的新鲜度。

LFU的原理与实现机制

  1. 普通队列
    :LFU算法通过记录数据项的访问频次来工作。当缓存容量达到上限时,系统将会淘汰访问频次最低的数据项。这种方法基于一个假设,即在一段时间内被访问频次较少的数据,未来被访问的几率同样较小。
  2. 数据结构选择
    :为实现O(1)的时间复杂度,这里LFU通常使用哈希表(存储key与节点数据)和双向链表(存储次数与key结构关系)结合的方式来实现。哈希表用于快速查找节点是否存在,而双向链表则用于根据访问频次组织数据项。此处双向链表用一个无限长度的
    LruCache
    代替。在
    remove
    或者改变频次的时候可以用O(1)的复杂度进行操作。一开始用
    HashSet<Key>
    来设计,因为在Rust中HashSet并不存在
    pop
    函数,在数据大量触发替代的时候随机选择一个元素效率太低。
  3. 节点管理
    :每个节点除了存储键值之外,还需附带访问频次信息。每次数据项被访问时,其对应的节点频次会增加;当需要淘汰时,寻找频次最低的节点进行移除或替换。

LFU与LRU的对比及使用场景

  • 算法侧重点差异
    :LRU侧重于数据的访问新鲜度,即最近未被访问的数据更容易被淘汰;而LFU更关注数据项的总访问频次,不频繁访问的数据被认为是低优先级的。
  • 适用场景的不同
    :LRU适合应对具有时间局部性的数据访问模式,例如某些顺序读取的场景;LFU则更适合数据访问模式较为平稳,且各个数据项访问频率差异明显的环境。
  • 实现复杂性对比
    :LRU的实现相对简单,通常只需要维护一个按照时间顺序排列的链表即可;而LFU需要同时考虑访问频次和时间两个维度,因此实现上更为复杂。

LFU算法的实际案例

  • 缓存系统中的应用
    :许多现代缓存系统中,如Redis,都实现了LFU作为缓存逐出策略之一,允许用户根据具体需求选择合适的淘汰算法。在数据负载高的时候可以允许配置
    maxmemory-policy

    volatile-lru|allkeys-lru|volatile-random|allkeys-random|volatile-ttl|volatile-lfu|allkeys-lfu|noeviction
  • 负载均衡算法
    :在分布式系统中,LFU也可以作为一种简单的负载均衡策略,将请求分散到不同的服务器上,避免单点过载。
  • 数据库查询优化
    :数据库管理系统中,LFU可以用来优化查询计划的缓存,减少磁盘I/O次数,提高重复查询的性能。

结构设计

与Lru的结构类似,K与V均用指针方式保存,避免在使用过程中出现Copy或者Clone的可能,提高性能。
注:该方法用了指针会相应的出现许多
unsafe
的代码,因为在Rsut中,访问指针都被认为是
unsafe
。我们也可以使用数组坐标模拟指针的方式来模拟。

节点设计

相对普通的Lru节点,我们需要额外存储次数数据。

/// Lfu节点数据
pub(crate) struct LfuEntry<K, V> {
    pub key: mem::MaybeUninit<K>,
    pub val: mem::MaybeUninit<V>,
    /// 访问总频次
    pub counter: usize,
    /// 带ttl的过期时间,单位秒
    /// 如果为u64::MAX,则表示不过期
    #[cfg(feature = "ttl")]
    pub expire: u64,
}

类设计

Lfu相对复杂度会比较高,这里维护了最大及最小的访问频次,方便遍历的时候高效

pub struct LfuCache<K, V, S> {
    map: HashMap<KeyRef<K>, NonNull<LfuEntry<K, V>>, S>,
    /// 因为HashSet的pop耗时太长, 所以取LfuCache暂时做为平替
    times_map: HashMap<u8, LruCache<KeyRef<K>, (), DefaultHasher>>,
    cap: usize,
    /// 最大的访问频次 
    max_freq: u8,
    /// 最小的访问频次
    min_freq: u8,
    /// 总的访问次数
    visit_count: usize,
    /// 初始的访问次数
    default_count: usize,
    /// 每多少次访问进行一次衰减
    reduce_count: usize,

    /// 下一次检查的时间点,如果大于该时间点则全部检查是否过期
    #[cfg(feature = "ttl")]
    check_next: u64,
    /// 每次大检查点的时间间隔,如果不想启用该特性,可以将该值设成u64::MAX
    #[cfg(feature = "ttl")]
    check_step: u64,
    /// 所有节点中是否存在带ttl的结点,如果均为普通的元素,则过期的将不进行检查
    #[cfg(feature = "ttl")]
    has_ttl: bool,
}
频次的设计

这此处频次我们设计成了一个u8类型,但是实际上我们访问次数肯定远远超过
u8::MAX
即255的数值。因为此处将访问总次数与频次做了一个映射,防止数据碎片太高及变动频次太频繁。
比如初始操作比较频繁的
0-10
分别映射成
0-6
如2或者3均映射到2,10-40映射到
7-10
。其本质的原理就是越高的访问频次越不容易被淘汰,相对来说4次或者5次很明显,但是100次和101次其实没多少差别。
这样子我们就可以将很高的梯度映射成一颗比较小的树,减少碎片化的操作。

/// 避免hash表爆炸, 次数与频次映射
fn get_freq_by_times(times: usize) -> u8 {
    lazy_static! {
        static ref CACHE_ARR: Vec<u8> = {
            let vec = vec![
                (0, 0, 0u8),
                (1, 1, 1u8),
                (2, 3, 2u8),
                (4, 4, 3u8),
                (5, 5, 4u8),
                (6, 7, 5u8),
                (8, 9, 6u8),
                (10, 12, 7u8),
                (13, 16, 8u8),
                (16, 21, 9u8),
                (22, 40, 10u8),
                (41, 79, 11u8),
                (80, 159, 12u8),
                (160, 499, 13u8),
                (500, 999, 14u8),
                (999, 1999, 15u8),
            ];
            let mut cache = vec![0;2000];
            for v in vec {
                for i in v.0..=v.1 {
                    cache[i] = v.2;
                }
            }
            cache
        };
        static ref CACHE_LEN: usize = CACHE_ARR.len();
    };
    if times < *CACHE_LEN {
        return CACHE_ARR[times];
    }
    if times < 10000 {
        return 16;
    } else if times < 100000 {
        return 17;
    } else if times < 1000000 {
        return 18;
    } else {
        return 19;
    }
}

这里用懒初始化,只有该函数第一次被调用的时候才会初始化这static代码,且只会初始化一次,增加访问的速度。

reduce_count
的设计

假设一段时间内某个元素访问特别多,如
algorithm-rs
访问了100000次,接下来很长的一段时间内他都没有出现过,如果普通的Lfu的淘汰规则,那么他将永远的保持在访问频次100000次,基本上属于很难淘汰。那么他将长久的占用了我们的数据空间。
针对这种情况此处设计了降权的模式,假设
reduce_count=100000
,那么就每10w次访问,将对历史的存量数据访问次数进行降权即新次数=原次数/2,那么在第一次降权后,
algorithm-rs
将变成了
50000
,其的权重将被削减。在一定访问的之后如果都没有该元素的访问最后他将会被淘汰。
visit_count
将当前访问的次数进行记录,一旦大于
reduce_count
将进行一轮降权并重新计算。

default_count
的设计

由于存在降权的,那么历史的数据次数可能会更低的次数。那么我们将插入的每个元素赋予初始次数,以防止数据在刚插入的时候就被淘汰。此处默认的访问次数为5。如果历史经历了降权,那么将会有可能存在数据比5小的数据,将优先被淘汰。

初始化

首先初始化对象,初始化map及空的双向链表:

impl<K, V, S> LfuCache<K, V, S> {
    /// 提供hash函数
    pub fn with_hasher(cap: usize, hash_builder: S) -> LfuCache<K, V, S> {
        let cap = cap.max(1);
        let map = HashMap::with_capacity_and_hasher(cap, hash_builder);
        Self {
            map,
            times_map: HashMap::new(),
            visit_count: 0,
            max_freq: 0,
            min_freq: u8::MAX,
            reduce_count: cap.saturating_mul(100),
            default_count: 4,
            cap,
            #[cfg(feature = "ttl")]
            check_step: DEFAULT_CHECK_STEP,
            #[cfg(feature = "ttl")]
            check_next: get_milltimestamp()+DEFAULT_CHECK_STEP * 1000,
            #[cfg(feature = "ttl")]
            has_ttl: false,
        }
    }
}

此处
min_freq > max_freq
在循环的时候将不会进行任何循环,表示没有任何元素。

元素插入及删除

插入对象,分已在缓存内和不在缓存内与Lru的类似,此处主要存在可能操作的列表变化问题

fn try_fix_entry(&mut self, entry: *mut LfuEntry<K, V>) {
    unsafe {
        if get_freq_by_times((*entry).counter) == get_freq_by_times((*entry).counter + 1) {
            self.visit_count += 1;
            (*entry).counter += 1;
        } else {
            self.detach(entry);
            self.attach(entry);
        }
    }
}

假如访问次数从10次->变成11次,但是他的映射频次并没有发生变化,此处我们仅仅需要将元素的次数+1即可,不用移动元素的位置。

attach 其中附到节点上:

fn attach(&mut self, entry: *mut LfuEntry<K, V>) {
    unsafe {
        self.visit_count += 1;
        (*entry).counter += 1;
        let freq = get_freq_by_times((*entry).counter);
        self.max_freq = self.max_freq.max(freq);
        self.min_freq = self.min_freq.min(freq);
        self.times_map
            .entry(freq)
            .or_default()
            .reserve(1)
            .insert((*entry).key_ref(), ());

        self.check_reduce();
    }
}

附到节点时我们将会改变
min_freq
,
max_freq
,并将该元素放入到对应的频次里预留足够的空间
reserve(1)
。并在最后检测是否降权
self.check_reduce()

detach 从队列中节点剥离

/// 从队列中节点剥离
fn detach(&mut self, entry: *mut LfuEntry<K, V>) {
    unsafe {
        let freq = get_freq_by_times((*entry).counter);
        self.times_map.entry(freq).and_modify(|v| {
            v.remove(&(*entry).key_ref());
        });
    }
}

此处我们仅仅移除节点key信息,这里使用的是LruCache,移除也是O(1)的时间复杂度。但是此处我们不维护
min_freq

max_freq
因为不确定是否当前是否维一,此处维护带来的收益较低,故不做维护。

check_reduce 降权

/// 从队列中节点剥离
fn check_reduce(&mut self) {
    if self.visit_count >= self.reduce_count {
        let mut max = 0;
        let mut min = u8::MAX;
        for (k, v) in self.map.iter() {
            unsafe {
                let node = v.as_ptr();
                let freq = get_freq_by_times((*node).counter);
                (*node).counter /= 2;
                let next = get_freq_by_times((*node).counter);
                max = max.max(next);
                min = min.min(next);
                if freq != next {
                    self.times_map.entry(freq).and_modify(|v| {
                        v.remove(k);
                    });
                    self.times_map
                        .entry(next)
                        .or_default()
                        .reserve(1)
                        .insert((*node).key_ref(), ());
                }
            }
        }
        self.max_freq = max;
        self.min_freq = min;
        self.visit_count = 0;
    }
}

当前降权后将重新初始化
min_freq

max_freq
,将当前的所有的频次/2,此算法的复杂度为O(n)。

replace_or_create_node 替换节点
fn replace_or_create_node(&mut self, k: K, v: V) -> (Option<(K, V)>, NonNull<LfuEntry<K, V>>) {
    if self.len() == self.cap {
        for i in self.min_freq..=self.max_freq {
            if let Some(val) = self.times_map.get_mut(&i) {
                if val.is_empty() {
                    continue;
                }
                let key = val.pop_unusual().unwrap().0;
                let old_node = self.map.remove(&key).unwrap();
                let node_ptr: *mut LfuEntry<K, V> = old_node.as_ptr();

                let replaced = unsafe {
                    (
                        mem::replace(&mut (*node_ptr).key, mem::MaybeUninit::new(k))
                            .assume_init(),
                        mem::replace(&mut (*node_ptr).val, mem::MaybeUninit::new(v))
                            .assume_init(),
                    )
                };
                unsafe {
                    (*node_ptr).counter = self.default_count;
                }
                return (Some(replaced), old_node);
            }
        }
        unreachable!()
    } else {
        (None, unsafe {
            NonNull::new_unchecked(Box::into_raw(Box::new(LfuEntry::new_counter(
                k,
                v,
                self.default_count,
            ))))
        })
    }
}

当元素数据满时,我们将做淘汰算法,此处我们将从
min_req

max_req
做遍历,并将最小的频次的pop掉最后一个元素。此处如果我们不需护
min_req

max_req
那么将会最坏的情况为0-255,即256次循环。

其它操作

  • pop
    移除栈顶上的数据,最近使用的
  • pop_last
    移除栈尾上的数据,最久未被使用的
  • contains_key
    判断是否包含key值
  • raw_get
    直接获取key的值,不会触发双向链表的维护
  • get
    获取key的值,并维护双向链表
  • get_mut
    获取key的值,并可以根据需要改变val的值
  • retain
    根据函数保留符合条件的元素
  • get_or_insert_default
    获取或者插入默认参数
  • get_or_insert_mut
    获取或者插入对象,可变数据
  • set_ttl
    设置元素的生存时间
  • del_ttl
    删除元素的生存时间,表示永不过期
  • get_ttl
    获取元素的生存时间
  • set_check_step
    设置当前检查lru的间隔

如何使用

在cargo.toml中添加

[dependencies]
algorithm = "0.1"
示例

use algorithm::LfuCache;
fn main() {
    let mut lru = LfuCache::new(3);
    lru.insert("hello", "algorithm");
    lru.insert("this", "lru");
    lru.set_reduce_count(100);
    assert!(lru.get_visit(&"hello") == Some(5));
    assert!(lru.get_visit(&"this") == Some(5));
    for _ in 0..98 {
        let _ = lru.get("this");
    }
    lru.insert("hello", "new");
    assert!(lru.get_visit(&"this") == Some(51));
    assert!(lru.get_visit(&"hello") == Some(3));
    let mut keys = lru.keys();
    assert!(keys.next()==Some(&"this"));
    assert!(keys.next()==Some(&"hello"));
    assert!(keys.next() == None);
}

完整项目地址

https://github.com/tickbh/algorithm-rs

结语

综上所述,LFU算法通过跟踪数据项的访问频次来决定淘汰对象,适用于数据访问频率差异较大的场景。与LRU相比,LFU更能抵御偶发性的大量访问请求对缓存的冲击。然而,LFU的实现较为复杂,需要综合考虑效率和公平性。在实际应用中,应当根据具体的数据访问模式和系统需求,灵活选择和调整缓存算法,以达到最优的性能表现。

大家好,我是码农先森。

现状

传统的 PHP-FPM 也是多进程模型的的运行方式,但每个进程只能处理完当前请求,才能接收下一个请求。而且对于 PHP 脚本来说,只是接收请求和响应请求,并不参与网络通信。对数据库资源的操作,也是一次请求一次有效,用完即销毁不能复用,在系统高负载的情况下对数据库等资源的消耗会很大,能承受的并发量有限。

Swoole 的出现给 PHP 带来了一种新的运行方式,完全接管了 PHP-FPM 的功能,并且弥补了 PHP 在异步网络通信领域的空白。Swoole 提供了 PHP 的全生命周期管理,此外 Swoole 的常驻进程模式,也能够高效的利用资源,比如可以建立数据库连接池、共享内存变量等。还有 Swoole 中能够支撑高并发的利器「协程」,更加使 PHP 的性能上了一个新的台阶,甚至在某些特定场景下都可以与 Go 语言的性能相媲美。

虽说 Swoole 给 PHP 带来了很大的性能提升,但也还是一个基于多进程模型的异步通信扩展,多进程的模式也存在着许多的问题,比如跨进程间的通信、进程间的资源共享等问题。简而言之,多进程会带来一定的系统资源消耗及产生新的问题。

因此 Swoole 官方为了解决多进程的问题,引进了多线程的支持,这意味着 v6 版本之后,Swoole 将会变成单进程多线程的运行模式。

v6 新特性

根据 Swoole 作者韩天峰发布的预告,在 v6 版本中增加多线程的支持。其中多线程的实现是基于 PHP 的 ZTS 机制和 TSRM API,在 PHP 层面隔离所有全局变量,实现线程安全。Swoole v6 的多线程将是真正的多线程实现,在单进程的模式下所有的 PHP 程序代码均是在多核并行执行,能够高效的利用好 CPU 资源。

v6 版本还提供了线程安全的 Map 和 ArrayList 数据结构,可以实现跨线程的数据共享读写。在 Server 端的 Event Worker、Task Worker、User Process 等将全部替换为 线程的运行方式,在同一个进程空间内执行,彻底摒弃了多进程的模式。

当然新的特性势必会带来新的开销,对于 Map 等共享的数据结构在多线程的模式下需要加锁,来避免数据竞争,可能会损耗一些性能。

以下是列举的一些线程相关的 API 方法:

  • use Swoole\Thread 线程对象。
  • use Swoole\Thread\Map 线程安全下的 Map 数据结构。
  • use Swoole\Thread\ArrayList 线程安全下的 ArrayList 数据结构。
  • Swoole\Thread::getId() 获取当前线程的 ID。
  • Swoole\Thread::getArguments() 获取父线程传递给子线程的参数列表。
  • Swoole\Thread::join() 等待子线程退出,请注意 $thread 对象销毁时会自动执行 join() ,这可能会导致进程阻塞。
  • Swoole\Thread::joinable() 检测子线程是否已退出。
  • Swoole\Thread::detach() 使子线程独立运行,不再需要 Thread::join()。
  • Swoole\Thread::HARDWARE_CONCURRENCY 硬件层支持的并行线程数量。
  • Swoole\Thread::$id 获取子线程的 ID。
  • Swoole\Thread::exec() 开启一个新的线程。

最后

自 Swoole 从 2012 年发布第一个版本开始,就扛起了 PHP 领域异步通信的大旗,但这多年以来 Swoole 的发展也是实属不易。还记得刚开始时的异步回调模式的套娃式编程方式,开发起来异常艰难,到后来的同步式编程,直接降低了PHP程序员的学习门槛,让 PHP 在实时通信、物联网通信、游戏开发等领域也能大展拳脚,同时在 PHP 的发展史上也产生了重大的影响。

随着 Go 语言在编程界的持续火热,Swoole 常常被 PHP 程序员拿来和 Go 语言一决高下,总是被诟病 Swoole 无法有效利用多核 CPU、进程间的通信困难等问题。话又说回来,Swoole 作为一个 PHP 的扩展程序和天生具有高性能的 Go 语言自然是不可比拟的,但 Swoole 也是在逐渐的向 Go 语言靠近,比如 Swoole 中也使用了「go、channel」关键词来实现协程及通信通道,虽说底层的实现机制还是大不相同的。

当然 Swoole 也在不断地努力持续优化,就像将要推出的 v6 版本增加多线程的支持,来改变目前多进程的局面。至于这个版本对 PHP 发展来说有没有很大的影响,我认为影响有限。但对 Swoole 的发展还是有很大的影响,毕竟以后再也不用受多进程的困扰了,这也是一大进步。

在 Web 领域作为世界上最好的语言,尽管 PHP 近年来的发展不尽如人意,但作为一名 PHPer 也有必要和有义务一起来维护和推动 PHP 生态的发展。


欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。

前言

众所周知,在vue中使用scoped可以避免父组件的样式渗透到子组件中。使用了scoped后会给html增加自定义属性
data-v-x
,同时会给组件内CSS选择器添加对应的属性选择器
[data-v-x]
。这篇我们来讲讲vue是如何给CSS选择器添加对应的属性选择器
[data-v-x]
。注:本文中使用的vue版本为
3.4.19

@vitejs/plugin-vue
的版本为
5.0.4

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

看个demo

我们先来看个demo,代码如下:

<template>
  <div class="block">hello world</div>
</template>

<style scoped>
.block {
  color: red;
}
</style>

经过编译后,上面的demo代码就会变成下面这样:

<template>
  <div data-v-c1c19b25 class="block">hello world</div>
</template>

<style>
.block[data-v-c1c19b25] {
  color: red;
}
</style>

从上面的代码可以看到在div上多了一个
data-v-c1c19b25
自定义属性,并且css的属性选择器上面也多了一个
[data-v-c1c19b25]

可能有的小伙伴有疑问,为什么生成这样的代码就可以避免样式污染呢?

.block[data-v-c1c19b25]
:这里面包含两个选择器。
.block
是一个类选择器,表示class的值包含
block

[data-v-c1c19b25]
是一个属性选择器,表示存在
data-v-c1c19b25
自定义属性的元素。

所以只有class包含
block
,并且存在
data-v-c1c19b25
自定义属性的元素才能命中这个样式,这样就能避免样式污染。

并且由于在同一个组件里面生成的
data-v-x
值是一样的,所以在同一组件内多个html元素只要class的值包含
block
,就可以命中
color: red
的样式。

接下来我将通过debug的方式带你了解,vue是如何在css中生成
.block[data-v-c1c19b25]
这样的属性选择器。

@vitejs/plugin-vue

还是一样的套路启动一个debug终端。这里以
vscode
举例,打开终端然后点击终端中的
+
号旁边的下拉箭头,在下拉中点击
Javascript Debug Terminal
就可以启动一个
debug
终端。
debug-terminal

假如
vue
文件编译为
js
文件是一个毛线团,那么他的线头一定是
vite.config.ts
文件中使用
@vitejs/plugin-vue
的地方。通过这个线头开始
debug
我们就能够梳理清楚完整的工作流程。
vite-config

vuePlugin函数

我们给上方图片的
vue
函数打了一个断点,然后在
debug
终端上面执行
yarn dev
,我们看到断点已经停留在了
vue
函数这里。然后点击
step into
,断点走到了
@vitejs/plugin-vue
库中的一个
vuePlugin
函数中。我们看到简化后的
vuePlugin
函数代码如下:

function vuePlugin(rawOptions = {}) {
  return {
    name: "vite:vue",
    // ...省略其他插件钩子函数
    transform(code, id, opt) {
      // ..
    }
  };
}

@vitejs/plugin-vue
是作为一个
plugins
插件在vite中使用,
vuePlugin
函数返回的对象中的
transform
方法就是对应的插件钩子函数。vite会在对应的时候调用这些插件的钩子函数,vite每解析一个模块都会执行一次
transform
钩子函数。更多vite钩子相关内容
查看官网

我们这里只需要看
transform
钩子函数,解析每个模块时调用。

由于解析每个文件都会走到
transform
钩子函数中,但是我们只关注
index.vue
文件是如何解析的,所以我们给
transform
钩子函数打一个条件断点。如下图:
conditional-breakpoint

然后点击Continue(F5),
vite
服务启动后就会走到
transform
钩子函数中打的断点。我们可以看到简化后的
transform
钩子函数代码如下:

function transform(code, id, opt) {
  const { filename, query } = parseVueRequest(id);
  if (!query.vue) {
    return transformMain(
      code,
      filename,
      options.value,
      this,
      ssr,
      customElementFilter.value(filename)
    );
  } else {
    const descriptor = getDescriptor(filename);
    if (query.type === "style") {
      return transformStyle(
        code,
        descriptor,
        Number(query.index || 0),
        options.value
      );
    }
  }
}

首先调用
parseVueRequest
函数解析出当前要处理的文件的
filename

query
,在debug终端来看看此时这两个的值。如下图:
query

从上图中可以看到
filename
为当前处理的vue文件路径,
query
的值为空数组。所以此时代码会走到
transformMain
函数中。

transformMain
函数

将断点走进
transformMain
函数,在我们这个场景中简化后的
transformMain
函数代码如下:

async function transformMain(code, filename, options) {
  const { descriptor } = createDescriptor(filename, code, options);

  const { code: templateCode } = await genTemplateCode(
    descriptor
    // ...省略
  );

  const { code: scriptCode } = await genScriptCode(
    descriptor
    // ...省略
  );

  const stylesCode = await genStyleCode(
    descriptor
    // ...省略
  );

  const output = [scriptCode, templateCode, stylesCode];
  let resolvedCode = output.join("\n");
  return {
    code: resolvedCode,
  };
}

我们在
通过debug搞清楚.vue文件怎么变成.js文件
文章中已经深入讲解过
transformMain
函数了,所以这篇文章我们不会深入到
transformMain
函数中使用到的每个函数中。

首先调用
createDescriptor
函数根据当前vue文件的code代码字符串生成一个
descriptor
对象,简化后的
createDescriptor
函数代码如下:

const cache = new Map();

function createDescriptor(
  filename,
  source,
  { root, isProduction, sourceMap, compiler, template }
) {
  const { descriptor, errors } = compiler.parse(source, {
    filename,
    sourceMap,
    templateParseOptions: template?.compilerOptions,
  });
  const normalizedPath = slash(path.normalize(path.relative(root, filename)));
  descriptor.id = getHash(normalizedPath + (isProduction ? source : ""));
  cache.set(filename, descriptor);
  return { descriptor, errors };
}

首先调用
compiler.parse
方法根据当前vue文件的code代码字符串生成一个
descriptor
对象,此时的
descriptor
对象主要有三个属性
template

scriptSetup

style
,分别对应的是vue文件中的
<template>
模块、
<template setup>
模块、
<style>
模块。

然后调用
getHash
函数给
descriptor
对象生成一个
id
属性,
getHash
函数代码如下:

import { createHash } from "node:crypto";
function getHash(text) {
  return createHash("sha256").update(text).digest("hex").substring(0, 8);
}

从上面的代码可以看出id是根据vue文件的路径调用node的
createHash
加密函数生成的,这里生成的id就是scoped生成的自定义属性
data-v-x
中的
x
部分。

然后在
createDescriptor
函数中将生成的
descriptor
对象缓存起来,关于
descriptor
对象的处理就这么多了。

接着在
transformMain
函数中会分别以
descriptor
对象为参数执行
genTemplateCode

genScriptCode

genStyleCode
函数,分别得到编译后的render函数、编译后的js代码、编译后的style代码。

编译后的render函数如下图:
templateCode

从上图中可以看到template模块已经编译成了render函数

编译后的js代码如下图:
scriptCode

从上图中可以看到script模块已经编译成了一个名为
_sfc_main
的对象,因为我们这个demo中script模块没有代码,所以这个对象是一个空对象。

编译后的style代码如下图:
stylesCode

从上图中可以看到style模块已经编译成了一个import语句。

最后就是使用换行符
\n

templateCode

scriptCode

stylesCode
拼接起来就是vue文件编译后的js文件啦,如下图:
resolvedCode

想必细心的同学已经发现有地方不对啦,这里的style模块编译后是一条import语句,并不是真正的css代码。这条import语句依然还是import导入的
index.vue
文件,只是加了一些额外的query参数。

?vue&type=style&index=0&lang.css
:这个query参数表明当前import导入的是vue文件的css部分。

还记得我们前面讲过的
transform
钩子函数吗?vite每解析一个模块都会执行一次
transform
钩子函数,这个import导入vue文件的css部分,当然也会触发
transform
钩子函数的执行。

第二次执行
transform
钩子函数

当在浏览器中执行vue文件编译后的js文件时会触发
import "/Users/xxx/index.vue?vue&type=style&index=0&lang.css"
语句的执行,导致再次执行
transform
钩子函数。

transform
钩子函数代码如下:

function transform(code, id, opt) {
  const { filename, query } = parseVueRequest(id);
  if (!query.vue) {
    return transformMain(
      code,
      filename,
      options.value,
      this,
      ssr,
      customElementFilter.value(filename)
    );
  } else {
    const descriptor = getDescriptor(filename);
    if (query.type === "style") {
      return transformStyle(
        code,
        descriptor,
        Number(query.index || 0),
        options.value
      );
    }
  }
}

由于此时的
query
中是有
vue
字段,所以
!query.vue
的值为false,这次代码就不会走进
transformMain
函数中了。在
else
代码在先执行
getDescriptor
函数拿到
descriptor
对象,
getDescriptor
函数代码如下:

function getDescriptor(filename) {
  const _cache = cache;
  if (_cache.has(filename)) {
    return _cache.get(filename);
  }
}

我们在第一次执行
transformMain
函数的时候会去执行
createDescriptor
函数,他的作用是根据当前vue文件的code代码字符串生成一个
descriptor
对象,并且将这个
descriptor
对象缓存起来了。在
getDescriptor
函数中就是将缓存的
descriptor
对象取出来。

由于
query
中有
type=style
,所以代码会走到
transformStyle
函数中。

transformStyle
函数

接着将断点走进
transformStyle
函数,代码如下:

async function transformStyle(code, descriptor, index, options) {
  const block = descriptor.styles[index];
  const result = await options.compiler.compileStyleAsync({
    ...options.style,
    filename: descriptor.filename,
    id: `data-v-${descriptor.id}`,
    source: code,
    scoped: block.scoped,
  });

  return {
    code: result.code,
  };
}

从上面的代码可以看到
transformStyle
函数依然不是干活的地方,而是调用的
@vue/compiler-sfc
包暴露出的
compileStyleAsync
函数。

在调用
compileStyleAsync
函数的时候有三个参数需要注意:
source

id

scoped

source
字段的值为
code
,值是当前css代码字符串。

id
字段的值为
data-v-${descriptor.id}
,是不是觉得看着很熟悉?没错他就是使用
scoped
后vue帮我们自动生成的html自定义属性
data-v-x
和css选择属性选择器
[data-v-x]

其中的
descriptor.id
就是在生成
descriptor
对象时根据vue文件路径加密生成的id。

scoped
字段的值为
block.scoped
,而
block
的值为
descriptor.styles[index]
。由于一个vue文件可以写多个style标签,所以
descriptor
对象的
styles
属性是一个数组,分包对应多个style标签。我们这里只有一个
style
标签,所以此时的
index
值为0。
block.scoped
的值为style标签上面是否有使用
scoped

直到进入
compileStyleAsync
函数之前代码其实一直都还在
@vitejs/plugin-vue
包中执行,真正干活的地方是在
@vue/compiler-sfc
包中。

@vue/compiler-sfc

接着将断点走进
compileStyleAsync
函数,代码如下:

function compileStyleAsync(options) {
  return doCompileStyle({
    ...options,
    isAsync: true,
  });
}

从上面的代码可以看到实际干活的是
doCompileStyle
函数。

doCompileStyle
函数

接着将断点走进
doCompileStyle
函数,在我们这个场景中简化后的
doCompileStyle
函数代码如下:

import postcss from "postcss";

function doCompileStyle(options) {
  const {
    filename,
    id,
    scoped = false,
    postcssOptions,
    postcssPlugins,
  } = options;
  const source = options.source;
  const shortId = id.replace(/^data-v-/, "");
  const longId = `data-v-${shortId}`;
  const plugins = (postcssPlugins || []).slice();

  if (scoped) {
    plugins.push(scopedPlugin(longId));
  }

  const postCSSOptions = {
    ...postcssOptions,
    to: filename,
    from: filename,
  };
  let result;
  try {
    result = postcss(plugins).process(source, postCSSOptions);
    return result.then((result) => ({
      code: result.css || "",
      // ...省略
    }));
  } catch (e: any) {
    errors.push(e);
  }
}


doCompileStyle
函数中首先使用
const
定义了一堆变量,我们主要关注
source

longId

其中的
source
为当前css代码字符串,
longId
为根据vue文件路径加密生成的id,值的格式为
data-v-x
。他就是使用
scoped
后vue帮我们自动生成的html自定义属性
data-v-x
和css选择属性选择器
[data-v-x]

接着就是判断
scoped
是否为true,也就是style中使用有使用scoped。如果为true,就将
scopedPlugin
插件push到
plugins
数组中。从名字你应该猜到了这个plugin插件就是用于处理css scoped的。

最后就是执行
result = postcss(plugins).process(source, postCSSOptions)
拿到经过
postcss
转换编译器处理后的css。

可能有的小伙伴对
postcss
不够熟悉,我们这里来简单介绍一下。

postcss
是 css 的 transpiler(转换编译器,简称转译器),它对于 css 就像 babel 对于 js 一样,能够做 css 代码的分析和转换。同时,它也提供了插件机制来做自定义的转换。

在我们这里主要就是用到了
postcss
提供的插件机制来完成css scoped的自定义转换,调用
postcss
的时候我们传入了
source
,他的值是style模块中的css代码。并且传入的
plugins
插件数组中有个
scopedPlugin
插件,这个自定义插件就是vue写的用于处理css scoped的插件。

在执行
postcss
对css代码进行转换之前我们在debug终端来看看此时的css代码是什么样的,如下图:
before-postcss

从上图可以看到此时的css代码还是和我们源代码是一样的,并没有css选择属性选择器
[data-v-x]

scopedPlugin
插件

scopedPlugin
插件在我们这个场景中简化后的代码如下:

const scopedPlugin = (id = "") => {
  return {
    postcssPlugin: "vue-sfc-scoped",
    Rule(rule) {
      processRule(id, rule);
    },
    // ...省略
  };
};

这里的id就是我们在
doCompileStyle
函数中传过来的
longId
,也就是生成的css选择属性选择器
[data-v-x]
中的
data-v-x

在我们这个场景中只需要关注
Rule
钩子函数,当
postcss
处理到选择器开头的规则就会走到
Rule
钩子函数。

我们这里需要在使用了scoped后给css选择器添加对应的属性选择器
[data-v-x]
,所以我们需要在插件中使用
Rule
钩子函数,在处理css选择器时手动给选择器后面塞一个属性选择器
[data-v-x]


Rule
钩子函数打个断点,当
postcss
处理到我们代码中的
.block
时就会走到断点中。在debug终端看看
rule
的值,如下图:
rule

从上图中可以看到此时
rule.selector
的值为
.block
,是一个class值为
block
的类选择器。

processRule
函数

将断点走进
processRule
函数中,在我们这个场景中简化后的
processRule
函数代码如下:

import selectorParser from "postcss-selector-parser";

function processRule(id: string, rule: Rule) {
  rule.selector = selectorParser((selectorRoot) => {
    selectorRoot.each((selector) => {
      rewriteSelector(id, selector, selectorRoot);
    });
  }).processSync(rule.selector);
}

前面我们讲过
rule.selector
的值为
.block
,通过重写
rule.selector
的值可以将当前css选择器替换为一个新的选择器。在
processRule
函数中就是使用
postcss-selector-parser
来解析一个选择器,进行处理后返回一个新的选择器。

processSync
方法的作用为接收一个选择器,然后在回调中对解析出来的选择器进行处理,最后将处理后的选择器以字符串的方式进行返回。

在我们这里
processSync
方法接收的选择器是字符串
.block
,经过回调函数处理后返回的选择器字符串就变成了
.block[data-v-c1c19b25]

我们接下来看
selectorParser
回调函数中的代码,在回调函数中会使用
selectorRoot.each
去遍历解析出来的选择器。

为什么这里需要去遍历呢?

答案是css选择器可以这样写:
.block.demo
,如果是这样的选择器经过解析后,就会被解析成两个选择器,分别是
.block

.demo

在each遍历中会调用
rewriteSelector
函数对当前选取器进行重写。

rewriteSelector
函数

将断点走进
rewriteSelector
函数,在我们这个场景中简化后的代码如下:

function rewriteSelector(id, selector) {
  let node;
  const idToAdd = id;

  selector.each((n) => {
    node = n;
  });

  selector.insertAfter(
    node,
    selectorParser.attribute({
      attribute: idToAdd,
      value: idToAdd,
      raws: {},
      quoteMark: `"`,
    })
  );
}


rewriteSelector
函数中each遍历当前
selector
选择器,给
node
赋值。将断点走到each遍历之后,我们在debug终端来看看
selector
选择器和
node
变量。如下图:
selector

在这里
selector
是container容器,
node
才是具体要操作的选择器节点。

比如我们这里要执行的
selector.insertAfter
方法就是在
selector
容器中在一个指定节点后面去插入一个新的节点。这个和操作浏览器DOM API很相似。

我们再来看看要插入的节点,
selectorParser.attribute
函数的作用是创建一个attribute属性选择器。在我们这里就是创建一个
[data-v-x]
的属性选择器,如下图:
attribute

所以这里就是在
.block
类选择器后面插入一个
[data-v-c1c19b25]
的属性选择器。

我们在debug终端来看看执行
insertAfter
函数后的
selector
选择器,如下图:
after-selector

将断点逐层走出,直到
processRule
函数中。我们在debug终端来看看此时被重写后的
rule.selector
字符串的值是什么样的,如下图
after-postcss

原来
rule.selector
的值为
.block
,通过重写
rule.selector
的值可以将
.block
类选择器替换为一个新的选择器,而这个新的选择器是在原来的
.block
类选择器后面再塞一个
[data-v-c1c19b25]
属性选择器。

总结

这篇文章我们讲了当使用scoped后,vue是如何给组件内CSS选择器添加对应的属性选择器
[data-v-x]
。主要分为两部分,分别在两个包里面执行。

  • 第一部分为在
    @vitejs/plugin-vue
    包内执行。


    • 首先会根据当前vue文件的路径进行加密算法生成一个id,这个id就是添加的属性选择器
      [data-v-x]
      中的
      x

    • 然后就是执行
      transformStyle
      函数,这个
      transformStyle
      并不是实际干活的地方,他调用了
      @vue/compiler-sfc
      包的
      compileStyleAsync
      函数。并且传入了
      id

      code
      (css代码字符串)、
      scoped
      (是否在style中使用
      scoped
      )。

  • 第二部分在
    @vue/compiler-sfc
    包执行。


    • compileStyleAsync
      函数依然不是实际干活的地方,而是调用了
      doCompileStyle
      函数。


    • doCompileStyle
      函数中,如果
      scoped
      为true就向
      plugins
      数组中插入一个
      scopedPlugin
      插件,这个是vue写的
      postcss
      插件,用于处理css scoped。然后使用
      postcss
      转换编译器对css代码进行转换。


    • postcss
      处理到选择器开头的规则就会走到
      scopedPlugin
      插件中的
      Rule
      钩子函数中。在
      Rule
      钩子函数中会执行
      processRule
      函数。


    • processRule
      函数中会使用
      postcss-selector-parser
      包将当前选择器替换为一个新的选择器,新的选择器和原来的选择器的区别是在后面会添加一个属性选择器
      [data-v-x]
      。其中的
      x
      就是根据当前vue文件的路径进行加密算法生成的
      id

在下一篇文章中我们会讲vue是如何给html元素增加自定义属性
data-v-x

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

本文分享自华为云社区《
Spring高手之路20——深入理解@EnableAspectJAutoProxy的力量
》,作者: 砖业洋__。

1. 初始调试代码

面向切面编程(
AOP
)是一种编程范式,用于增强软件模块化,通过将横切关注点(如事务管理、安全等)分离出业务逻辑。
Spring AOP

Spring
框架中实现
AOP
的一种方式,它通过代理机制在运行时向对象动态地添加增强。
AspectJ
是一种更强大的
AOP
实现,它通过编译时和加载时织入,提供了比
Spring AOP
更丰富的增强选项。本文将探索如何通过
Spring AOP
进行简单的
AOP
配置和实现。

后续源码分析就用这个前置通知的代码调试

package com.example.demo.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public classMyAspect {
@Before(
"execution(* com.example.demo.service.MyService.performAction(..))")public voidbeforeAdvice(JoinPoint joinPoint) {
System.
out.println("Before method:" +joinPoint.getSignature().getName());
}
}
package com.example.demo.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public classAppConfig {
}
package com.example.demo.service;

import org.springframework.stereotype.Service;
//一个简单的服务类 @Servicepublic classMyService {public voidperformAction() {
System.
out.println("Performing an action");
}
}
package com.example.demo;

import com.example.demo.service.MyService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
//主应用类 @ComponentScan(basePackages = "com.example.demo")public classDemoApplication {public static voidmain(String[] args) {
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(DemoApplication.class);
MyService myService
= context.getBean(MyService.class);
myService.performAction();
//调用方法,触发AOP增强 }
}

2. 源码跟踪分析

2.1 初探@EnableAspectJAutoProxy

上面代码中,
AppConfig
配置类里有个
@EnableAspectJAutoProxy
注解,前面说过,
@EnableAspectJAutoProxy
注解告诉
Spring
框架去寻找带有
@Aspect
注解的类,
Spring AOP
通过读取
@EnableAspectJAutoProxy
注解的属性来配置代理的行为。

下面用时序图来展示通过
@EnableAspectJAutoProxy
注解启用面向切面编程(
AOP
)的过程。

解读:

1、启动ApplicationContext:

应用 (
App
) 向
ApplicationContext
发送消息以启动
Spring
的应用上下文。这是
Spring
应用的初始化阶段,负责设置
Spring
的核心功能,包括
Bean
的加载和管理。

2、加载配置类:

ApplicationContext
接着加载 配置类 (
ConfigClass
)。这个配置类包含了应用的配置信息,如
Bean
定义和
AOP
支持的相关注解等。

3、检测@EnableAspectJAutoProxy:

配置类完成加载后,检查是否包含
@EnableAspectJAutoProxy
注解。此注解是启用
Spring AOP
代理的关键,它指示
Spring
框架自动为符合条件的
Bean
创建
AOP
代理。

4、注册AspectJAutoProxyCreator:

一旦检测到
@EnableAspectJAutoProxy
注解,
ApplicationContext
会注册
AspectJAutoProxyCreator (APC)
。这个组件是一个
BeanPostProcessor
,它在
Spring
容器的
bean
初始化阶段介入,自动检测容器中所有带有
@Aspect
注解的类,并为这些类创建代理。这个代理创建过程不仅包括实现通知逻辑的织入,还涉及对被代理对象的调用进行拦截,确保在执行目标方法前后能够执行相应的通知(
advice
)。

5、扫描和注册Beans:

ApplicationContext
继续扫描应用中的其他
Bean
,并将它们注册到
Spring
容器中。这包括普通的
Bean
和那些可能成为
AOP
代理目标的
Bean

6、识别@Aspect注解:


Bean
的扫描过程中,识别出带有
@Aspect
注解的
Bean

AspectBean
)。这些
Bean
定义了
AOP
的切面,如通知方法(
advice
),指定在某些方法执行前后或抛出异常时执行。

7、请求创建代理:

当识别到
@Aspect
注解的
Bean
时,这些
Bean
会向
AspectJAutoProxyCreator
发出请求,要求创建相应的代理。

8、调用创建代理:

AspectJAutoProxyCreator
收到创建代理的请求后,调用代理工厂 (
ProxyFactory
) 来构建具体的代理实例。

9、构建代理Bean:

代理工厂 根据
AspectJAutoProxyCreator
的指示,为
@Aspect
注解的
Bean
创建代理。这些代理将封装原
Bean
,并在调用原
Bean
的方法时,按照
@Aspect
定义执行相应的前置、后置或异常通知。

10、注册代理Bean:

创建完成的代理
Bean

ProxyBean
)被注册回
ApplicationContext
,替换或增加到原有的
Bean
配置中。

11、完成Bean加载和初始化:

所有
Bean
,包括新注册的代理
Bean
,都被加载和初始化后,
ApplicationContext
向应用 (
App
) 发送消息,表示
Bean
加载和初始化工作已完成,应用可以开始执行。

来看看源码,这里可以看到
@Import
导入了一个注册器
AspectJAutoProxyRegistrar

@EnableAspectJAutoProxy
注解启用
Spring
的自动代理机制,该注解有两个重要的属性配置:
proxyTargetClass

exposeProxy

proxyTargetClass
属性默认为
false
,此时
Spring
使用
JDK
动态代理来代理接口。如果设置为
true
,则
Spring
将使用
CGLIB
来代理类,这在目标对象没有实现接口时特别有用。
exposeProxy
属性默认为
false
,如果设置为
true
,允许通过
AopContext
类访问当前的代理对象,这在需要在目标对象内部方法调用自身被代理的方法时非常有用。

2.2 registerBeanDefinitions方法和时序图分析

本节源码都基于
5.3.16
分析。

这段代码主要涉及
2.1
节时序图中的“加载配置类”和“注册
AspectJAutoProxyCreator
”这两个步骤。

AspectJAutoProxyRegistrar
类的
registerBeanDefinitions
方法打上断点调试。

这个方法主要负责根据
@EnableAspectJAutoProxy
注解的设置来配置
Spring AOP
的行为,包括是否使用
CGLIB
进行类代理而不是基于接口的
JDK
代理,以及是否允许在被代理的对象内部通过
AopContext
访问代理对象。这两个设置对于控制
Spring AOP
的行为至关重要,特别是在处理复杂的代理场景和高级
AOP
功能时。

代码提出来分析:

//注册Bean定义的方法,通过读取注解元数据和操作Bean定义注册表进行配置
public voidregisterBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {//检查是否已经注册了AspectJ自动代理创建器,如果没有,则进行注册
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);//从导入的类的注解元数据中获取@EnableAspectJAutoProxy注解的属性
    AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);//检查是否成功获取@EnableAspectJAutoProxy注解的属性
    if (enableAspectJAutoProxy != null) {//检查@EnableAspectJAutoProxy注解的proxyTargetClass属性是否为true
        if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {//如果proxyTargetClass为true,则强制AOP代理创建器使用CGLIB来进行类代理
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
//检查@EnableAspectJAutoProxy注解的exposeProxy属性是否为true if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {//如果exposeProxy为true,则强制AOP代理创建器暴露代理对象,使其能在被代理的对象内部通过AopContext访问 AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
}
}
}

这个方法的两个入参说明一下:

  1. importingClassMetadata

    AnnotationMetadata
    类型的实例,它持有关于当前正在被处理的类的注解信息。这里用来检索有关
    @EnableAspectJAutoProxy
    注解的信息,这些信息决定了如何配置
    AOP
    代理的行为(是否使用
    CGLIB
    代理以及是否暴露代理对象)。

  2. registry

    BeanDefinitionRegistry
    类型的实例,它是一个用于注册
    Bean
    定义的接口。通过这个注册表,可以在运行时向
    Spring
    应用上下文添加新的
    Bean
    定义或修改现有的
    Bean
    定义。这里用于实际调整
    AOP
    配置,如注册
    AOP
    代理创建器,以及设置代理创建器的行为(根据
    @EnableAspectJAutoProxy
    的属性值)。这些操作直接影响了
    Spring AOP
    如何在运行时创建和管理
    AOP
    代理。

如果流程太抽象,那么用时序图补充

这个时序图展示了
Spring AOP
配置的完整流程,从检查和注册自动代理创建器,到根据
@EnableAspectJAutoProxy
注解的设置调整
Spring
的代理行为。此过程确保了应用的
AOP
配置能够根据给定的注解属性正确地执行,无论是使用更高性能的
CGLIB
代理,还是暴露代理以供内部访问。

完整的时序图解释

1. 方法调用开始

调用者 (
Caller
)触发
registerBeanDefinitions
方法(
RBD
),这通常发生在应用的配置阶段。

2. 检查并注册自动代理创建器

registerBeanDefinitions

AopConfigUtils

AopCU
)发起调用,检查是否已注册
AspectJ
自动代理创建器,或者是否需要注册新的或更新现有的代理创建器。

3. 自动代理创建器的注册和更新

  • AopConfigUtils

    Registry

    Reg
    )执行实际的注册或更新操作。
  • Registry
    完成更新后反馈给
    AopConfigUtils
  • AopConfigUtils
    然后将结果返回给
    registerBeanDefinitions

4. 获取@EnableAspectJAutoProxy注解的属性

registerBeanDefinitions
接着从
AnnotationConfigUtils

ACU
)获取
@EnableAspectJAutoProxy
注解的相关属性,这些属性决定代理的行为。

5. 根据属性设置代理方式

  • 如果注解的
    proxyTargetClass
    属性为真,意味着需要使用
    CGLIB
    来进行类代理而不是基于接口的代理。
  • registerBeanDefinitions
    要求
    AopConfigUtils
    强制使用
    CGLIB
    代理。
  • AopConfigUtils
    更新
    Registry
    中相关
    Bean
    定义的设置以使用
    CGLIB
  • Registry
    确认设置已更新,然后
    AopConfigUtils
    通知
    registerBeanDefinitions
    配置完成。

6. 设置是否暴露代理

  • 如果注解的
    exposeProxy
    属性为真,意味着需要暴露代理,允许通过
    AopContext
    访问当前代理。
  • registerBeanDefinitions
    要求
    AopConfigUtils
    强制暴露代理。
  • AopConfigUtils

    Registry
    中进行相应设置更新。
  • Registry
    确认设置已更新,然后
    AopConfigUtils
    通知
    registerBeanDefinitions
    配置完成。

7. 配置流程完成

一旦所有设置完成,
registerBeanDefinitions
向调用者报告配置流程已完成。

2.3 registerOrEscalateApcAsRequired方法和时序图分析

看到刚刚第一句注册后置处理器,我们来详细看看

这段代码主要与
2.1
节时序图中的“注册
AspectJAutoProxyCreator
”步骤相对应。
AspectJAutoProxyCreator
是由
Spring
内部管理的一个自动代理创建器,用于基于
AspectJ
的注解来创建
AOP
代理。它与用户定义的切面(使用
@Aspect
注解的类)相区分,后者指定了具体的通知(如
@Before
,
@AfterReturning
等)和切点表达式。在
Spring

AOP
实现中,代理创建器负责实际的代理对象创建工作,而用户定义的切面提供了应用于这些代理对象的通知逻辑。具体而言,它描述了如何在
Spring

ApplicationContext
中检查并可能更新或注册一个新的自动代理创建器(
AspectJAutoProxyCreator
)。

直接分析
registerOrEscalateApcAsRequired
方法

//定义一个用于注册或升级自动代理创建器的静态方法
private static BeanDefinition registerOrEscalateApcAsRequired(Class<?>cls, BeanDefinitionRegistry registry, @Nullable Object source) {//断言,确保传入的registry不为空
    Assert.notNull(registry, "BeanDefinitionRegistry must not be null");//检查容器是否已经包含名为"org.springframework.aop.config.internalAutoProxyCreator"的Bean定义
    if (registry.containsBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator")) {//获取已存在的自动代理创建器的Bean定义
        BeanDefinition apcDefinition = registry.getBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator");//检查当前注册的自动代理创建器类名是否与传入的cls类名不同
        if (!cls.getName().equals(apcDefinition.getBeanClassName())) {//找到当前自动代理创建器的优先级
            int currentPriority =findPriorityForClass(apcDefinition.getBeanClassName());//找到需要注册的自动代理创建器的优先级
            int requiredPriority =findPriorityForClass(cls);//比较两个优先级,若已注册的优先级低,则更新为新的自动代理创建器类
            if (currentPriority <requiredPriority) {
apcDefinition.setBeanClassName(cls.getName());
}
}
//若已存在自动代理创建器且不需要升级,则返回null return null;
}
else{//若未注册自动代理创建器,则创建一个新的RootBeanDefinition实例 RootBeanDefinition beanDefinition = newRootBeanDefinition(cls);//设置bean定义的来源 beanDefinition.setSource(source);//设置bean定义的属性,这里设置"order"属性为最小整数值,表示最高优先级 beanDefinition.getPropertyValues().add("order", Integer.MIN_VALUE);//设置bean定义的角色,通常ROLE_INFRASTRUCTURE表示框架内部使用的组件 beanDefinition.setRole(2);//在注册表中注册名为"org.springframework.aop.config.internalAutoProxyCreator"的新自动代理创建器Bean定义 registry.registerBeanDefinition("org.springframework.aop.config.internalAutoProxyCreator", beanDefinition);//返回新创建的Bean定义 returnbeanDefinition;
}
}

这个方法主要用于控制
Spring AOP
框架中的自动代理创建器(
AutoProxyCreator
)的注册与优先级升级,确保
AOP
功能按预期工作,特别是在有多个自动代理创建器可能存在时确保正确的配置和行为优先级。

自动代理创建器(
AutoProxyCreator
)是一个核心组件,根据配置(如注解、
XML
配置或程序的指定)识别需要增强的
Bean
,并自动为这些
Bean
创建代理。这些代理可以在方法调用前后添加额外的行为,而不修改原有代码的基础上,实现如安全检查、事务管理、日志记录等横切关注点。

如果流程太抽象,那么用时序图补充

这个时序图展示了
registerOrEscalateApcAsRequired
方法如何根据已存在的自动代理创建器
Bean
定义的情况来决定执行的操作。通过检查、比较和可能的更新或创建操作,它确保了最适合的类被用于自动代理创建器。如果当前注册的自动代理创建器足够适合,不会进行更改;如果不适合,会进行更新或创建新的
Bean
定义,以保证系统配置的最优化。

1. 开始调用

调用者发起对
registerOrEscalateApcAsRequired
方法的调用。该方法接收三个参数:类(
cls
),注册表(
registry
)和源信息(
source
)。

2. 检查Bean定义是否存在

registerOrEscalateApcAsRequired

BeanDefinitionRegistry
查询是否已存在名为 “
internalAutoProxyCreator
” 的
Bean
定义。

3. 处理已存在的Bean定义

  • 如果
    BeanDefinitionRegistry
    确认
    Bean
    定义已存在(返回
    true
    ),
    registerOrEscalateApcAsRequired

    BeanDefinitionRegistry
    请求获取该
    Bean
    定义。
  • BeanDefinitionRegistry

    BeanDefinition
    返回给
    registerOrEscalateApcAsRequired
  • registerOrEscalateApcAsRequired
    使用返回的
    BeanDefinition
    检查并比较当前
    Bean
    的类与新传入的类
    cls
    的优先级。

4. 决定是否更新Bean定义

  • 如果新类
    cls
    的优先级更高,
    registerOrEscalateApcAsRequired
    会在
    BeanDefinition
    中更新类名为新类
    cls.getName()
  • 更新操作完成后,
    BeanDefinition
    通知
    BeanDefinitionRegistry
    更新已完成。
  • 如果当前已注册的类的优先级足够高或相同,不需要进行更新,
    registerOrEscalateApcAsRequired
    直接返回
    null
    给调用者。

5. 处理不存在的Bean定义

  • 如果
    BeanDefinitionRegistry
    确认没有找到名为 “
    internalAutoProxyCreator
    ” 的
    Bean
    定义(返回
    false
    ),
    registerOrEscalateApcAsRequired
    将创建一个新的
    BeanDefinition
  • 新创建的
    BeanDefinition
    被注册到
    BeanDefinitionRegistry
  • 注册完成后,
    BeanDefinitionRegistry
    确认新的
    BeanDefinition
    已注册。
  • registerOrEscalateApcAsRequired
    最终将新创建的
    BeanDefinition
    返回给调用者。

点击关注,第一时间了解华为云新鲜技术~