Spring AOP原理篇:我用上我的洪荒之力来帮你彻底了解aop注解@EnableAspectJAutoProxy的原理

本文主要是介绍Spring AOP原理篇:我用上我的洪荒之力来帮你彻底了解aop注解@EnableAspectJAutoProxy的原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

  • 在上篇文章spring aop使用篇:熟悉使用前置通知、后置通知、返回通知、异常通知,并了解其特性中我们知道了如何使用aop以及其的一些特性,同时还提出来如下所述的疑问点:

    1、源码中是如何将我们定义的各种通知与目标方法绑定起来的

    2、aop代理对象生成的策略

    3、我们的aop代理对象的执行顺序是怎样的

    接下来,我们继续以上篇文章的测试案例为例,从源码的角度来分析这三个点。废话不多说,直接开干!

一、源码中是如何将我们定义的各种通知与目标方法绑定起来的

  • 如果让各位自己来实现aop你会采用什么方式?(3分钟后过后…)不管是使用哪种方式来实现,最终一定会使用代理设计模式。毫无疑问,我们使用代理对象来增强目标对象,然后在执行目标对象的方法之前或者之后,我们可以执行很多自定义的操作:比如前置操作、后置操作等等。那spring是如何实现aop的呢?大家都知道,在使用spring 的aop之前,我们需要定义切面、切点、通知,有了这三个东西后,spring才能知道要对哪个目的地(切点)做逻辑增强(通知)。接下来,咱们来分析下spring的做法。

1.1 @EnableAspectJAutoProxy注解的含义

  • 在使用spring时,我们通常在配置类中添加@EnableAspectJAutoProxy注解的话,项目的aop功能就会开启了。那这个注解在spring中主要做了什么事情呢?这与spring的**@Import扩展点相关(如果不熟悉的话,可以看我之前spring系列相关的文章),它主要是向spring容器导入了AspectJAutoProxyRegistrar**的bean,那这个bean做了哪些事情呢?详看下图:

    在这里插入图片描述

    如图所示:其主要是往spring容器中添加了一个类型为AnnotationAwareAspectJAutoProxyCreator,名称叫org.springframework.aop.config.internalAutoProxyCreator的beanDefinition(由于bean的名称太长,后续统一叫aopProxyCreator)。其次,会根据@EnableAspectJAutoProxy注解配置的proxyTargetClass属性和exposeProxy属性来填充aopProxyCreator bean的对应字段的属性。这两个属性就涉及到了spring aop最终会使用哪种代理方式生成代理对象,以及是否可以使用AopContext.currentProxy()的方式获取暴露出来的代理对象。

  • 分析到这,@EnableAspectJAutoProxy注解的功能就完事了。那我们接着要怎么分析呢?因为这个注解往spring容器中添加了aopProxyCreator的beanDefinition,最终spring容器肯定会去创建它,因为spring的很多扩展点的使用前提是,这个类得是一个spring bean。因此,我们现在去看看aopProxyCreator这个bean到底有什么特殊的地方。

1.2 aopProxyCreator 这个bean有什么特殊点

  • 在1.1章节中有说明aopProxyCreator这个bean的类型为:AnnotationAwareAspectJAutoProxyCreator,那我们看下这个类的关系继承图

    在这里插入图片描述

    不看不知道,一看吓一跳。这个bean实现了Aware和BeanPostProcessor接口。毫无疑问,这属于spring扩展点范畴,spring会在合适的时机来调用这两个接口的对应方法。这里以创建aopProxyCreator这个bean为例简单描述下每个接口的大致含义即调用时机:

    扩展点类型作用触发时机
    BeanFactoryAware可以获取到spring容器中的bean工厂对象,(需要实现setBeanFactory方法创建当前bean时触发。eg:当前要创建aopProxyCreator这个bean,则会触发
    BeanPostProcessorbean后置处理器扩展点的顶级接口,在创建bean后(完成了依赖注入),回调所有后置处理器的before和after方法(可以需要实现postProcessBeforeInitialization和postProcessAfterInitialization方法若aopProxyCreator这个bean被创建,会对后续spring创建的所有的bean生效
    InstantiationAwareBeanPostProcessorBeanPostProcessor的子类,对BeanPostProcessor做了扩展,可以在实例化bean之前(postProcessBeforeInstantiation)和之后(postProcessAfterInstantiation)做自定义的事情。这里的实例化仅仅是创建bean对象,还没有完成依赖注入操作。同时,如果在postProcessAfterInstantiation方法返回false的话,spring容器将不会对这个bean做依赖注入操作。(可以实现postProcessBeforeInstantiation和postProcessAfterInstantiation方法若aopProxyCreator这个bean被创建,会对后续spring创建的所有的bean生效
    SmartInstantiationAwareBeanPostProcessorInstantiationAwareBeanPostProcessor的子类,对InstantiationAwareBeanPostProcessor做了扩展。这个后置处理器的最重要的方法为:determineCandidateConstructors,最终会调用到此方法来确定当前bean要使用哪个构造方法来实例化bean(可以实现determineCandidateConstructors方法若aopProxyCreator这个bean被创建,会对后续spring创建的所有的bean生效

    上述后置处理器,那他们的执行顺序是怎样的呢?是不是不知所措?别灰心,我都为你准备好了,其执行顺序是这样的(为什么要了解这些后置处理器的执行顺序?因为知道执行顺序后,就能知道每个扩展点在aop功能中起到了什么样的作用):

    第一个阶段:
    创建aopProxyCreator这个bean时触发:BeanFactoryAware#setBeanFactory第二个阶段:
    在aopProxyCreator这个bean创建后(已经加入spring容器),后续创建出来的所有bean在执行到AnnotationAwareAspectJAutoProxyCreator后置处理器时是按照如下顺序执行的:InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation> SmartInstantiationAwareBeanPostProcessor#determineCandidateConstructors> InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation> BeanPostProcessor#postProcessBeforeInitialization> BeanPostProcessor#postProcessAfterInitialization
    

    但实际上,只有InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation和BeanPostProcessor#postProcessAfterInitialization这两个方法的实现和aop有关系。因此,我们着重分析下这两个方法。

1.3 AnnotationAwareAspectJAutoProxyCreator的postProcessBeforeInstantiation做了什么事

  • 其源码如下所示:

    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {Object cacheKey = getCacheKey(beanClass, beanName);if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {if (this.advisedBeans.containsKey(cacheKey)) {return null;}if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {  // @1this.advisedBeans.put(cacheKey, Boolean.FALSE);return null;}}// Create proxy here if we have a custom TargetSource.// Suppresses unnecessary default instantiation of the target bean:// The TargetSource will handle target instances in a custom fashion.TargetSource targetSource = getCustomTargetSource(beanClass, beanName);if (targetSource != null) {if (StringUtils.hasLength(beanName)) {this.targetSourcedBeans.add(beanName);}Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;}return null;
    }
    
  • @1处的代码比较核心,其主要逻辑是:判断当前bean是否为基础类(Advice.class、Pointcut.class、Advisor.class、AopInfrastructureBean.class),如果不是基础类的话,会执行shouldSkip方法的逻辑。而在shouldSkip这个方法中,会去找项目中所有的切面以及切面内部定义的通知(包括实现了Advisor接口的切面和通知、自定义的切面和通知)。那这个找通知的过程是如何执行的呢?这里的源码执行逻辑比较复杂,我们可以先看执行结果:

    在这里插入图片描述

    在此方法执行完毕后,我们已经把AspectDefinition.java这个类中定义的各种通知已经找出来并转化成Advisor的类型存在了一个list中,那这一步spring到底是怎么做的呢?我们画图来详细分析下shouldSkip方法的执行流程:

    在这里插入图片描述

    根据图中的分析可知,当shouldSkip方法执行完毕后,整个spring容器中所有的切面中定义的通知都会被缓存到内存中,大家可能会有疑惑,我怎么知道哪个通知适用于哪个切点呢?还记得在将通知转换成AspectJExpressionPointcut时有保存每个通知中的表达式吧?eg:@After(value = "pointcutAnnotation()") 将获取到里面的 pointcutAnnotation() 字符串。有了这个的话,我在找对应关系时,我看下哪个切点的方法名是这个,不就对应上了吗?

    同时,在图中有提到InstantiationModelAwarePointcutAdvisorImpl对象。它的本质是一个Advisor。在找通知的过程中,会将每个通知包装成InstantiationModelAwarePointcutAdvisorImpl类型的对象,其中这个对象中有一个特别重要的属性,就是:instantiatedAdvice,这个属性是Advice的类型的。其中,针对我们的通知类型,会将它转化成对应的Advice。其转换类型如下表中所示:

    切面中定义的通知类型切面中定义的通知类型的注解Advice类型
    AtBefore@BeforeAspectJMethodBeforeAdvice
    AtAfter@AfterAspectJAfterAdvice
    AtAfterReturning@AfterReturningAspectJAfterReturningAdvice
    AtAfterThrowing@AfterThrowingAspectJAfterThrowingAdvice
    AtAround@AroundAspectJAroundAdvice

    其对应的源码如下所示:

    AbstractAspectJAdvice springAdvice;switch (aspectJAnnotation.getAnnotationType()) {// 前置通知case AtBefore:springAdvice = new AspectJMethodBeforeAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);break;// 后置通知case AtAfter:springAdvice = new AspectJAfterAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);break;// 返回通知case AtAfterReturning:springAdvice = new AspectJAfterReturningAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();if (StringUtils.hasText(afterReturningAnnotation.returning())) {springAdvice.setReturningName(afterReturningAnnotation.returning());}break;// 异常通知case AtAfterThrowing:springAdvice = new AspectJAfterThrowingAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {springAdvice.setThrowingName(afterThrowingAnnotation.throwing());}break;// 环绕通知case AtAround:springAdvice = new AspectJAroundAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);break;// 切点逻辑case AtPointcut:if (logger.isDebugEnabled()) {logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");}return null;default:throw new UnsupportedOperationException("Unsupported advice type on method: " + candidateAdviceMethod);
    }
    
  • 结论:AnnotationAwareAspectJAutoProxyCreator的postProcessBeforeInstantiation方法的主要核心在于将容器中所有的切面对应的通知都扫描出来并包装成InstantiationModelAwarePointcutAdvisorImpl类型的对象并添加到缓存中这里要注意:不管是自定义的切面、还是实现了Advisor接口的切面都会被扫描出来)。一种预热机制,先把数据准备好,后续需要时直接再从缓存中拿。

1.4 AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization做了什么事

  • 其源码及注释如下所示:

    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (!this.earlyProxyReferences.contains(cacheKey)) {return wrapIfNecessary(bean, beanName, cacheKey); // @1}}return bean;
    }
    
  • 这段源码的主要核心部分为:@1处的位置,这段代码就是创建代理对象的入口。其方法名也比较见名知意:包装如果有必要的话,如果你比较有经验并且知道静态代理的话。这块儿的做法也一样,其实就是生成了一个代理对象,然后将目标对象包裹在里面。那wrapIfNecessary这个方法到底做了什么事呢?其源码如下所示:

    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)) { // @1this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;}// Create proxy if we have advice.Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); // @2if (specificInterceptors != DO_NOT_PROXY) {this.advisedBeans.put(cacheKey, Boolean.TRUE);Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); // @3this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;}this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;
    }
    
  • 我们先看下@1指向的代码,有没有觉得很相似。没错,它在1.3章节说到的AnnotationAwareAspectJAutoProxyCreator的postProcessBeforeInstantiation处也出现了,这是为什么呢?还记得我们在1.3章节中总结的那几个扩展点的执行顺序吧?不管一个bean需不需要被代理,都会执行AnnotationAwareAspectJAutoProxyCreator后置处理器,而@1指向的代码的含义仅仅是判断当前bean是否需要被代理而已,如果一个bean不需要被代理,那应该AnnotationAwareAspectJAutoProxyCreator后置处理器不应该对bean做任何操作。

  • @2指向的代码逻辑为:看这个bean是否有定义切面,如果有,则把对应的通知都找出来,最终转化成一个个的拦截器,后续生成代理对象时,内部要维护这些拦截器,以实现调用我们定义好的通知的目的。

  • @3指向的代码逻辑为:真实创建代理对象的逻辑,最终会调用到如下代码创建代理对象

    // 方法坐标:org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy@Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {// config.isProxyTargetClass() 获取的就是注解中@EnableAspectJAutoProxy配置的proxyTargetClass属性// 如果这里设置为true,则走内部逻辑,否则走@2处指向的代码处:使用jdk动态代理生成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);}return new ObjenesisCglibAopProxy(config);}else {return new JdkDynamicAopProxy(config);  // @2}
    }
    

    这里可以衍生出来几个面试题:

    Q1:使用@EnableAspectJAutoProxy注解开启aop功能后,默认是使用jdk动态代理还是cglib生成代理对象?

    A1:默认是jdk动态代理生成


    Q2:使用@EnableAspectJAutoProxy注解开启aop功能后,如何让spring使用cglib生成代理对象,如何让spring使用jdk动态代理生成代理对象?

    A2:满足两个条件:目标类不是一个接口 并且 设置proxyTargetClass为true

    因为我们的@EnableAspectJAutoProxy注解并未指定使用cglib代理,因此,最终生成的代理对象类型为:org.springframework.aop.framework.JdkDynamicAopProxy(根据方法的返回值签名知道的)。

  • 总结:AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization方法主要作用就是:创建代理对象。

1.5 代理对象的执行过程

  • 在1.4章节中有说到,最终ObjectServiceImpl生成的代理对象类型为:org.springframework.aop.framework.JdkDynamicAopProxy。如果大家熟悉jdk动态代理的话,我们可以直接到JdkDynamicAopProxy类中找invoke方法。invoke方法就是我们代理对象的执行入口了

  • invoke方法的逻辑比较长,其中有对一些基础方法的判断,比如:对于Object类的equals、hashCode方法则不执行增强逻辑,直接执行目标方法。因此,剩下的就是需要被代理的逻辑了,下面的代码展示的就是代理方法的逻辑:

    // 方法坐标:org.springframework.aop.framework.JdkDynamicAopProxy#invoke@Override
    @Nullable
    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 retVal;// 还记得@EnableAspectJAutoProxy注解的exposeProxy属性吗,我们设置为true的话,就会将代理对象暴露到线程变量中if (this.advised.exposeProxy) {// 将代理对象放到ThreadLocal中,我们可以使用AopContext.currentProxy()获取到当前的代理对象oldProxy = AopContext.setCurrentProxy(proxy);setProxyContext = true;}// 获取真实的目标对象target = targetSource.getTarget();Class<?> targetClass = (target != null ? target.getClass() : null);// 这一步:获取当前执行的目标方法,已经为目标方法定义的一些通知,最终以拦截器的方式存储在list中List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); // @1if (chain.isEmpty()) {// 省略无关代码}else {// 构建链路的调用器invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); // @2// 开始执行链路上的方法retVal = invocation.proceed(); // @3}return retVal;} finally {if (target != null && !targetSource.isStatic()) {// Must have come from TargetSource.targetSource.releaseTarget(target);}if (setProxyContext) {// Restore old proxy.AopContext.setCurrentProxy(oldProxy);}}
    }
    
  • 上述@1处指定的代码处主要是获取一个方法调用链路,如果熟悉责任链设计模式的话,这一步相当于是构建所有的链路

  • 上述@2处指定的代码处主要是构建一个方法调用入口,如果熟悉责任链设计模式的话,这一步相当于是将所有的链路铺好(哪个链要放在第一个位置被调用,哪个链要放在第二个位置被调用、哪个链要放在最后一个位置被调用),等待被触发

  • 上述@3处指定的代码为链路的执行者,表示要开始执行链路上的所以方法了。

  • 这个链路的模样及执行过程见下图:

    在这里插入图片描述

    由上述图分析可知,想必大家明白在上篇文章spring aop使用篇:熟悉使用前置通知、后置通知、返回通知、异常通知,并了解其特性介绍的各种通知的特性有所了解了吧?首先,方法调用栈是从异常通知开始的,在异常通知中有一个大的try catch块,它能捕获到链路中抛出的异常,进而执行异常通知。其次,后置通知处于第二个链路中,由于内部有try finally块,而在finally块中执行的是后置通知,所以后置通知是一定会被执行的。再其次,返回通知位于第三个链路中,在返回通知中,并没有try finally代码块,因此返回通知不一定会被执行。最后,前置通知位于第四个链路中,其与目标方法的执行是同步的。通过上述的分析可知,只有前置通知是在目标方法执行之前触发的,剩下的后置通知、返回通知、异常通知都要等目标方法执行完毕后再根据代码的逻辑执行不同的通知逻辑。

二、总结

  • Spring AOP的原理,从源码的层面出发,从@EnableAspectJAutoProxy注解开始,到生成代理对象以及代理对象的执行顺序都总结了一遍,希望对你有所帮助!

  • 如果你觉得我的文章有用的话,欢迎点赞、收藏和关注。😆

  • I’m a slow walker, but I never walk backwards

这篇关于Spring AOP原理篇:我用上我的洪荒之力来帮你彻底了解aop注解@EnableAspectJAutoProxy的原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/730621

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于