Spring AOP源码深度解析

Spring AOP源码深度解析

本文将从源码层面深入剖析Spring AOP的实现原理,包括AOP核心组件、代理创建机制、通知执行流程以及与AspectJ的集成等关键内容。

一、Spring AOP核心组件

1. AOP核心接口体系

Spring AOP基于以下核心接口构建5:

java 复制代码
// 切面接口
public interface Aspect {
    // 定义切面相关信息
}

// 通知接口(Advice)
public interface Advice {
    // 通知是切面的具体实现逻辑
}

// 切入点接口(Pointcut)
public interface Pointcut {
    ClassFilter getClassFilter();
    MethodMatcher getMethodMatcher();
}

// 顾问接口(Advisor),组合了切入点和通知
public interface Advisor {
    Advice getAdvice();
    boolean isPerInstance();
}

// 增强器接口(IntroductionAdvisor)
public interface IntroductionAdvisor extends Advisor {
    ClassFilter getClassFilter();
    void validateInterfaces() throws IllegalArgumentException;
    Class<?>[] getInterfaces();
}

2. 通知类型体系

Spring AOP支持五种通知类型,每种类型都有对应的接口:

java 复制代码
// 前置通知
public interface MethodBeforeAdvice extends BeforeAdvice {
    void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}

// 后置通知
public interface AfterReturningAdvice extends AfterAdvice {
    void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable;
}

// 环绕通知
public interface MethodInterceptor extends Interceptor {
    Object invoke(MethodInvocation invocation) throws Throwable;
}

// 异常通知
public interface ThrowsAdvice extends AfterAdvice {
    // 无方法定义,需要自定义实现特定签名的方法
}

二、Spring AOP代理创建机制

1. AOP代理创建入口

Spring AOP代理创建的核心入口是AbstractAutoProxyCreator类,它实现了BeanPostProcessor接口,在Bean初始化后为符合条件的Bean创建代理对象5:

java 复制代码
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
        implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {

    @Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }

    protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        // 检查是否已经处理过
        if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        }
        if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        }
        if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }

        // 获取适用于当前Bean的通知器
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
        if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
            // 创建代理对象
            Object proxy = createProxy(
                    bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
        }

        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }
}

2. 代理类型选择机制

Spring AOP通过DefaultAopProxyFactory决定使用JDK动态代理还是CGLIB代理4:

java 复制代码
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        // 决定使用JDK动态代理还是CGLIB代理的逻辑
        if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: " +
                        "Either an interface or a target is required for proxy creation.");
            }
            // 如果目标类是接口或代理类本身,使用JDK动态代理
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                return new JdkDynamicAopProxy(config);
            }
            // 否则使用CGLIB代理
            return new ObjenesisCglibAopProxy(config);
        } else {
            // 默认使用JDK动态代理
            return new JdkDynamicAopProxy(config);
        }
    }

    // 判断目标类是否没有实现用户提供的接口
    private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
        Class<?>[] interfaces = config.getProxiedInterfaces();
        return (interfaces.length == 0 || (interfaces.length == 1 && SpringProxy.class.isAssignableFrom(interfaces[0])));
    }
}

3. JDK动态代理实现

JdkDynamicAopProxy实现了InvocationHandler接口,通过JDK的反射机制创建代理对象2:

java 复制代码
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {

    private final AdvisedSupport advised;

    @Override
    public Object getProxy(@Nullable ClassLoader classLoader) {
        Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
        findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
        // 创建JDK动态代理实例
        return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MethodInvocation invocation;
        Object oldProxy = null;
        boolean setProxyContext = false;

        TargetSource targetSource = this.advised.targetSource;
        Object target = null;

        try {
            // 处理Object类的方法
            if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
                return equals(args[0]);
            }
            else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
                return hashCode();
            }
            else if (method.getDeclaringClass() == DecoratingProxy.class) {
                return AopProxyUtils.ultimateTargetClass(this.advised);
            }
            else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
                    method.getDeclaringClass().isAssignableFrom(Advised.class)) {
                return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
            }

            Object retVal;

            // 设置代理上下文(如果需要)
            if (this.advised.exposeProxy) {
                oldProxy = AopContext.setCurrentProxy(proxy);
                setProxyContext = true;
            }

            // 获取目标对象
            target = targetSource.getTarget();
            Class<?> targetClass = (target != null ? target.getClass() : null);

            // 获取适合此方法的拦截器链
            List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

            // 如果没有拦截器,直接调用目标方法
            if (chain.isEmpty()) {
                Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
                retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
            }
            else {
                // 创建方法调用
                invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
                // 执行拦截器链
                retVal = invocation.proceed();
            }

            // 处理返回值
            Class<?> returnType = method.getReturnType();
            if (retVal != null && retVal == target && returnType != Object.class &&
                    returnType.isInstance(proxy) && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
                retVal = proxy;
            }
            return retVal;
        }
        finally {
            if (target != null && !targetSource.isStatic()) {
                targetSource.releaseTarget(target);
            }
            if (setProxyContext) {
                AopContext.setCurrentProxy(oldProxy);
            }
        }
    }
}

4. CGLIB代理实现

CglibAopProxy使用CGLIB库创建代理对象,适用于目标类没有实现接口的情况3:

java 复制代码
public class CglibAopProxy implements AopProxy, Serializable {

    private final AdvisedSupport advised;

    @Override
    public Object getProxy(@Nullable ClassLoader classLoader) {
        try {
            Class<?> rootClass = this.advised.getTargetClass();
            Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

            Class<?> proxySuperClass = rootClass;
            // 处理代理类
            if (ClassUtils.isCglibProxyClass(rootClass)) {
                proxySuperClass = rootClass.getSuperclass();
                Class<?>[] additionalInterfaces = rootClass.getInterfaces();
                for (Class<?> additionalInterface : additionalInterfaces) {
                    this.advised.addInterface(additionalInterface);
                }
            }

            // 验证并准备CGLIB增强器
            Enhancer enhancer = createEnhancer();
            if (classLoader != null) {
                enhancer.setClassLoader(classLoader);
                if (classLoader instanceof SmartClassLoader &&
                        ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
                    enhancer.setUseCache(false);
                }
            }
            enhancer.setSuperclass(proxySuperClass);
            enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
            enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
            enhancer.setStrategy(new ClassLoaderAwareUndeclaredThrowableStrategy(classLoader));

            // 创建回调数组
            Callback[] callbacks = getCallbacks(rootClass);
            enhancer.setCallbacks(callbacks);
            enhancer.setCallbackFilter(new ProxyCallbackFilter(
                    this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));

            // 创建并返回代理实例
            return createProxyClassAndInstance(enhancer, callbacks);
        }
        catch (CodeGenerationException | IllegalArgumentException ex) {
            throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
                    ": Common causes of this problem include using a final class or a non-visible class", ex);
        }
        catch (Throwable ex) {
            throw new AopConfigException("Unexpected AOP exception", ex);
        }
    }

    // 内部类MethodInterceptor实现了CGLIB的MethodInterceptor接口
    private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {

        private final AdvisedSupport advised;

        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            Object oldProxy = null;
            boolean setProxyContext = false;
            Object target = null;
            TargetSource targetSource = this.advised.getTargetSource();
            try {
                // 类似JDK动态代理的逻辑
                if (this.advised.exposeProxy) {
                    oldProxy = AopContext.setCurrentProxy(proxy);
                    setProxyContext = true;
                }
                target = targetSource.getTarget();
                Class<?> targetClass = (target != null ? target.getClass() : null);
                
                // 获取拦截器链
                List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
                Object retVal;
                
                // 执行拦截器链或直接调用目标方法
                if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
                    Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
                    retVal = methodProxy.invoke(target, argsToUse);
                }
                else {
                    retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
                }
                
                return retVal;
            }
            finally {
                // 清理工作
                if (target != null && !targetSource.isStatic()) {
                    targetSource.releaseTarget(target);
                }
                if (setProxyContext) {
                    AopContext.setCurrentProxy(oldProxy);
                }
            }
        }
    }
}

三、通知执行机制

1. 拦截器链的创建

Spring AOP将通知转换为拦截器链,然后按顺序执行。核心逻辑在AdvisedSupport类中:

java 复制代码
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) {
    MethodCacheKey cacheKey = new MethodCacheKey(method);
    List<Object> cached = this.methodCache.get(cacheKey);
    if (cached == null) {
        cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
                this, method, targetClass);
        this.methodCache.put(cacheKey, cached);
    }
    return cached;
}

2. 拦截器链的执行

通知执行的核心逻辑在ReflectiveMethodInvocation类的proceed()方法中,它实现了责任链模式1:

java 复制代码
public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable {

    protected final Object proxy;
    @Nullable
    protected final Object target;
    protected final Method method;
    protected Object[] arguments;
    @Nullable
    private final Class<?> targetClass;
    protected final List<Object> interceptorsAndDynamicMethodMatchers;
    private int currentInterceptorIndex = -1;

    @Override
    public Object proceed() throws Throwable {
        // 执行到了最后一个拦截器,直接调用目标方法
        if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
            return invokeJoinpoint();
        }

        // 获取下一个拦截器
        Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

        // 如果是动态方法匹配器,需要判断是否匹配
        if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
            InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
            Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
            if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
                return dm.interceptor.invoke(this);
            } else {
                // 不匹配,跳过此拦截器
                return proceed();
            }
        } else {
            // 静态拦截器,直接执行
            return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
        }
    }

    protected Object invokeJoinpoint() throws Throwable {
        return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
    }
}

3. 各种通知类型的实现

MethodBeforeAdviceInterceptor为例,展示前置通知的实现:

java 复制代码
public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable {

    private final MethodBeforeAdvice advice;

    public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {
        Assert.notNull(advice, "Advice must not be null");
        this.advice = advice;
    }

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        // 先执行前置通知
        this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
        // 再执行后续拦截器或目标方法
        return mi.proceed();
    }
}

环绕通知的实现(以AspectJAfterThrowingAdvice为例):

java 复制代码
public class AspectJAfterThrowingAdvice extends AbstractAspectJAdvice implements MethodInterceptor, AfterAdvice {

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        try {
            // 执行后续拦截器或目标方法
            return mi.proceed();
        } catch (Throwable ex) {
            // 如果有异常,检查是否是需要处理的异常类型
            if (shouldInvokeOnThrowing(ex)) {
                // 调用异常通知方法
                invokeAdviceMethod(getJoinPointMatch(), null, ex);
            }
            throw ex;
        }
    }

    private boolean shouldInvokeOnThrowing(Throwable ex) {
        return getAspectJAdviceMethod().getParameterTypes()[2].isAssignableFrom(ex.getClass());
    }
}

四、Spring AOP与AspectJ集成

1. @EnableAspectJAutoProxy注解分析

@EnableAspectJAutoProxy注解是Spring启用AspectJ风格AOP的核心5:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

    /**
     * 是否代理目标类,true使用CGLIB,false使用JDK动态代理
     */
    boolean proxyTargetClass() default false;

    /**
     * 是否暴露代理对象到ThreadLocal
     */
    boolean exposeProxy() default false;

}

2. AspectJAutoProxyRegistrar实现

java 复制代码
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata,
            BeanDefinitionRegistry registry) {

        // 注册AOP相关基础设施
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

        // 获取@EnableAspectJAutoProxy注解的属性
        AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy != null) {
            // 设置proxyTargetClass属性
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
            // 设置exposeProxy属性
            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
                AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
        }
    }
}

3. AnnotationAwareAspectJAutoProxyCreator分析

AnnotationAwareAspectJAutoProxyCreator是Spring AOP与AspectJ集成的核心类,它负责扫描@Aspect注解的类并创建代理:

java 复制代码
public class AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator {

    @Nullable
    private AspectJAdvisorFactory aspectJAdvisorFactory;

    @Nullable
    private BeanFactoryAspectJAdvisorsBuilder aspectJAdvisorsBuilder;

    @Override
    protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        super.initBeanFactory(beanFactory);
        if (this.aspectJAdvisorFactory == null) {
            this.aspectJAdvisorFactory = new ReflectiveAspectJAdvisorFactory(beanFactory);
        }
        this.aspectJAdvisorsBuilder = new BeanFactoryAspectJAdvisorsBuilderAdapter(beanFactory, this.aspectJAdvisorFactory);
    }

    @Override
    protected List<Advisor> findCandidateAdvisors() {
        // 先调用父类方法获取Spring AOP的Advisor
        List<Advisor> advisors = super.findCandidateAdvisors();
        // 再添加AspectJ的Advisor
        if (this.aspectJAdvisorsBuilder != null) {
            advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
        }
        return advisors;
    }
}

4. AspectJ注解解析

BeanFactoryAspectJAdvisorsBuilder负责扫描和解析@Aspect注解的类:

java 复制代码
public List<Advisor> buildAspectJAdvisors() {
    List<String> aspectNames = this.aspectBeanNames;

    if (aspectNames == null) {
        synchronized (this) {
            aspectNames = this.aspectBeanNames;
            if (aspectNames == null) {
                List<Advisor> advisors = new ArrayList<>();
                aspectNames = new ArrayList<>();
                // 获取所有Bean的名称
                String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                        this.beanFactory, Object.class, true, false);
                // 遍历所有Bean,查找@Aspect注解的类
                for (String beanName : beanNames) {
                    if (!isEligibleBean(beanName)) {
                        continue;
                    }
                    // 获取Bean的类型
                    Class<?> beanType = this.beanFactory.getType(beanName);
                    if (beanType == null) {
                        continue;
                    }
                    // 检查是否有@Aspect注解
                    if (this.advisorFactory.isAspect(beanType)) {
                        aspectNames.add(beanName);
                        AspectMetadata amd = new AspectMetadata(beanType, beanName);
                        if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
                            MetadataAwareAspectInstanceFactory factory =
                                    new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
                            // 解析Aspect类中的通知方法,创建Advisor
                            List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
                            if (this.beanFactory.isSingleton(beanName)) {
                                this.advisorsCache.put(beanName, classAdvisors);
                            } else {
                                this.aspectFactoryCache.put(beanName, factory);
                            }
                            advisors.addAll(classAdvisors);
                        }
                    }
                }
                this.aspectBeanNames = aspectNames;
                return advisors;
            }
        }
    }

    // 从缓存中获取Advisor
    if (aspectNames.isEmpty()) {
        return Collections.emptyList();
    }
    List<Advisor> advisors = new ArrayList<>();
    for (String aspectName : aspectNames) {
        List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
        if (cachedAdvisors != null) {
            advisors.addAll(cachedAdvisors);
        } else {
            MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
            if (factory != null) {
                advisors.addAll(this.advisorFactory.getAdvisors(factory));
            }
        }
    }
    return advisors;
}

五、Spring AOP与AspectJ的区别

特性 Spring AOP AspectJ
实现方式 基于动态代理(JDK/CGLIB) 编译时织入(CTW)、后编译织入或加载时织入(LTW)
支持的连接点 仅限方法执行 全面(方法、构造器、字段、异常处理点等)
性能 运行时开销较大 编译时处理,运行时无额外开销
功能范围 仅支持方法级别的切面 支持字段、构造器、方法等更细粒度切面
织入时机 运行时织入 编译时/加载时织入
学习曲线 较简单 较复杂
依赖 仅需Spring框架 需要AspectJ编译器或织入器
适用场景 适合大多数Spring应用 需要更强大切面功能或更高性能要求的场景

六、Spring AOP工作流程总结

Spring AOP的完整工作流程可以概括为以下几个主要步骤:

  1. AOP配置解析:解析@AspectJ注解或XML配置,识别切面、通知和切入点
  2. 注册自动代理创建器:通过@EnableAspectJAutoProxy注册AnnotationAwareAspectJAutoProxyCreator
  3. 扫描切面类:扫描并解析带有@Aspect注解的类,将通知方法转换为Advisor
  4. 创建代理对象:在Bean初始化后,AbstractAutoProxyCreator为符合条件的Bean创建代理
  5. 选择代理类型:根据目标类是否实现接口,选择JDK动态代理或CGLIB代理
  6. 执行增强逻辑:当调用代理对象的方法时,按照责任链模式执行各种通知

总结

Spring AOP是Spring框架的核心特性之一,它通过动态代理机制在运行时为目标对象创建代理,并在代理中织入增强逻辑。深入理解Spring AOP的源码实现,不仅有助于我们更好地使用AOP功能,也能让我们学习到优秀的设计模式和编程思想。

Spring AOP的设计非常灵活,它通过责任链模式处理各种通知,通过工厂模式创建代理对象,并与IoC容器紧密集成,为Java应用提供了强大的面向切面编程支持。

相关推荐
码路飞21 分钟前
GPT-5.3 Instant 终于学会好好说话了,顺手对比了下同天发布的 Gemini 3.1 Flash-Lite
java·javascript
序安InToo24 分钟前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy12324 分钟前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记27 分钟前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang0527 分钟前
VS Code 配置 Markdown 环境
后端
navms31 分钟前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang0531 分钟前
离线数仓的优化及重构
后端
Nyarlathotep011332 分钟前
gin01:初探gin的启动
后端·go
JxWang0532 分钟前
安卓手机配置通用多屏协同及自动化脚本
后端
JxWang0533 分钟前
Windows Terminal 配置 oh-my-posh
后端