文档章节

Spring AOP 源码解析:注解式切面增强机制

zhenchao
 zhenchao
发布于 05/15 14:53
字数 7135
阅读 2.3W
收藏 67

#程序员薪资揭榜#你做程序员几年了?月薪多少?发量还在么?>>>

IoC 和 AOP 被称为 Spring 两大基础模块,支撑着上层扩展的实现和运行。虽然 AOP 同样建立在 IoC 的实现基础之上,但是作为对 OOP(Object-Oriented Programing) 的补充,AOP(Aspect-Oriented Programming) 在程序设计领域拥有其不可替代的适用场景和地位。Spring AOP 作为 AOP 思想的实现,被誉为 Spring 框架的基础模块也算是实至名归。Spring 在 1.0 版本的时候就引入了对 AOP 的支持,并且随着版本的迭代逐渐提供了基于 XML 配置、注解,以及 schema 配置的使用方式,考虑到实际开发中使用注解配置的方式相对较多,所以本文主要分析注解式 AOP 的实现和运行机制。

注解式 AOP 示例

首先我们还是通过一个简单的示例演示一下注解式 AOP 的具体使用。假设我们声明了一个 IService 接口,并提供了相应的实现类 ServiceImpl,如下:

public interface IService {
    void sayHello();
    void sayHelloTo(String name);
    void sayByebye();
    void sayByebyeTo(String name);
}

@Service
public class ServiceImpl implements IService {

    @Override
    public void sayHello() {
        this.sayHelloTo("zhenchao");
    }

    @Override
    public void sayHelloTo(String name) {
        System.out.println("hello, " + name);
    }

    @Override
    public void sayByebye() {
        this.sayByebyeTo("zhenchao");
    }

    @Override
    public void sayByebyeTo(String name) {
        System.out.println("byebye, " + name);
    }

}

现在我们希望借助 Spring AOP 实现对方法调用的打点功能。首先我们需要定义一个切面:

@Aspect
@Component
public class MetricAspect {

    @Before("execution(* sayHello*(..))")
    public void beforeMetrics4sayHello(JoinPoint point) {
        System.out.println("[BEFORE] metrics for method: " + point.getSignature().getName());
    }

    @Around("execution(* say*(..))")
    public Object aroundMetrics4say(ProceedingJoinPoint point) throws Throwable {
        System.out.println("[AROUND] before metrics for method: " + point.getSignature().getName());
        Object obj = point.proceed();
        System.out.println("[AROUND] after metrics for method: " + point.getSignature().getName());
        return obj;
    }

    @After("execution(* sayByebye*(..))")
    public void afterMetrics4sayByebye(JoinPoint point) {
        System.out.println("[AFTER] metrics for method: " + point.getSignature().getName());
    }

}

通过 @Aspect 注解标记 MetricAspect 是一个切面,通过注解 @Before@After,以及 @Around,我们在切面中定义了相应的前置、后置,以及环绕增强。然后我们需要在 XML 配置中添加一行如下配置以启用注解式 AOP:

<aop:aspectj-autoproxy/>

现在,我们就算大功告成了。

当然,上面的实现只是注解式 AOP 使用的一个简单示例,并没有覆盖所有的特性。对于 Spring AOP 特性的介绍不属于本文的范畴,不过我们还是会在下面分析源码的过程中进行针对性的介绍。

注解式 AOP 实现机制

下面从启用注解式 AOP 的那一行配置切入,即 <aop:aspectj-autoproxy/> 标签。前面在分析 Spring IoC 实现的文章中,曾专门分析过 Spring 默认标签和自定义标签的解析过程。对于一个标签而言,除了标签的定义,还需要有对应的标签的解析器,并在 Spring 启动时将标签及其解析器注册到 Spring 容器中。标签 <aop:aspectj-autoproxy /> 的注册过程由 AopNamespaceHandler#init 方法实现:

// 注册 <aspectj-autoproxy/> 标签及其解析器
this.registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());

AspectJAutoProxyBeanDefinitionParser 类是标签 <aop:aspectj-autoproxy /> 的解析器,该类实现了 BeanDefinitionParser 接口,并实现了 BeanDefinitionParser#parse 接口方法,属于标准的标签解析器定义。Spring 容器在启动时会调用 AspectJAutoProxyBeanDefinitionParser#parse 方法解析标签,实现如下:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 注册标签解析器,默认使用 AnnotationAwareAspectJAutoProxyCreator
    AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
    // 解析 <aop:include /> 子标签,记录到 BeanDefinition 到 includePatterns 属性中
    this.extendBeanDefinition(element, parserContext);
    return null;
}

该方法做了两件事情:注册标签解析器和处理 <aop:include /> 子标签。本文我们重点来看标签解析器的注册过程,即 AopNamespaceUtils#registerAspectJAnnotationAutoProxyCreatorIfNecessary 方法:

public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary(ParserContext parserContext, Element sourceElement) {
    // 1. 注册或更新代理创建器 ProxyCreator 的 BeanDefinition 对象
    BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(
            parserContext.getRegistry(), parserContext.extractSource(sourceElement));
    // 2. 获取并处理标签的 proxy-target-class 和 expose-proxy 属性
    useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
    // 3. 注册组件,并发布事件通知
    registerComponentIfNecessary(beanDefinition, parserContext);
}

我们在代码注释中标明了该方法所做的 3 件事情,其中 1 和 2 是我们分析的关键,首先来看 1 过程所做的事情:

public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(
        BeanDefinitionRegistry registry, @Nullable Object source) {
    return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}

private static BeanDefinition registerOrEscalateApcAsRequired(
        Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) {

    Assert.notNull(registry, "BeanDefinitionRegistry must not be null");

    // 如果名为 org.springframework.aop.config.internalAutoProxyCreator 的 bean 已经在册
    if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
        BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
        // 已经在册的 ProxyCreator 与当前期望的类型不一致,则依据优先级进行选择
        if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
            int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
            int requiredPriority = findPriorityForClass(cls);
            // 选择优先级高的 ProxyCreator 更新注册
            if (currentPriority < requiredPriority) {
                apcDefinition.setBeanClassName(cls.getName());
            }
        }
        return null;
    }

    // 没有对应在册的 ProxyCreator,注册一个新的
    RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
    beanDefinition.setSource(source);
    beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
    beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
    return beanDefinition;
}

上述实现的逻辑还是挺简单的,即注册一个名为 org.springframework.aop.config.internalAutoProxyCreator 的 BeanDefinition,我们称之为代理创建器(ProxyCreator)。这里使用的默认实现为 AnnotationAwareAspectJAutoProxyCreator 类,如果存在多个候选实现,则选择优先级最高的进行注册。

接下来看一下过程 2,这一步主要是用来解析标签 <aop:aspectj-autoproxy/>proxy-target-classexpose-proxy 属性配置,由 AopNamespaceUtils#useClassProxyingIfNecessary 方法实现:

private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, @Nullable Element sourceElement) {
    if (sourceElement != null) {
        /*
         * 获取并处理 proxy-target-class 属性:
         * - false 表示使用 java 原生动态代理
         * - true 表示使用 CGLib 动态
         *
         * 但是对于一些没有接口实现的类来说,即使设置为 false 也会使用 CGlib 进行代理
         */
        boolean proxyTargetClass = Boolean.parseBoolean(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE));
        if (proxyTargetClass) {
            // 为之前注册的 ProxyCreator 添加一个名为 proxyTargetClass 的属性,值为 true
            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
        }

        /*
         * 获取并处理 expose-proxy 标签,实现对于内部方法调用的 AOP 增强
         */
        boolean exposeProxy = Boolean.parseBoolean(sourceElement.getAttribute(EXPOSE_PROXY_ATTRIBUTE));
        if (exposeProxy) {
            // 为之前注册的 ProxyCreator 添加一个名为 exposeProxy 的属性,值为 true
            AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
        }
    }
}

其中 proxy-target-class 属性用来配置是否使用 CGLib 代理,而 expose-proxy 属性则用来配置是否对内部方法调用启用 AOP 增强。属性 proxy-target-class 的作用大家应该都比较熟悉,下面介绍一下 expose-proxy 属性。前面给出的 AOP 示例中,我们在 IService#sayHello 方法中调用了 IService#sayHelloTo 方法,虽然两个方法都满足对应的 AOP 增强定义,但是只有 IService#sayHello 方法被增强了,这主要是因为 IService#sayHelloTo 方法是在对象内部调用的,调用该方法的对象并不是代理对象。如果期望内部调用时也能够被增强,我们需要配置 expose-proxy=true,并修改 IService#sayHello 方法对于 IService#sayHelloTo 方法的调用方式:

public void sayHello() {
    ((IService) AopContext.currentProxy()).sayHelloTo("zhenchao");
}

上面分析了这么多,总的说来就是向 Spring 容器中注册了一个 AnnotationAwareAspectJAutoProxyCreator 类型的 ProxyCreator,并将配置的 proxy-target-classexpose-proxy 属性添加到对应 BeanDefinition 的属性列表中。那么 AnnotationAwareAspectJAutoProxyCreator 到底是来做什么的呢?我们先来看一下它的类继承关系图:

Spring AOP Proxy Creator

从类继承关系图中可以看到该类实现了 BeanPostProcessor 接口,该接口定义如下:

public interface BeanPostProcessor {

    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

}

由之前对 Spring IoC 容器启动过程的分析,我们知道在容器启动过程中会在初始化 bean 实例的前后分别调用 BeanPostProcessor 中定义的这两个方法。针对这两个方法的实现主要位于继承链的 AbstractAutoProxyCreator 类中,并且主要是实现了 BeanPostProcessor#postProcessAfterInitialization 方法:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        // 如果 beanName 不为空则直接使用 beanName(FactoryBean 则使用 &{beanName}),否则使用 bean 的 className
        Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
        if (!this.earlyProxyReferences.contains(cacheKey)) {
            // 尝试对 bean 进行增强,创建返回增强后的代理对象
            return this.wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

该方法的核心在于调用 AbstractAutoProxyCreator#wrapIfNecessary 方法尝试基于 AOP 配置对当前 bean 进行增强,并返回增强后的代理对象。方法 AbstractAutoProxyCreator#wrapIfNecessary 的实现如下:

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // 已经处理过,直接返回
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
        return bean;
    }
    // 不需要进行增强的 bean 实例,直接跳过
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
        return bean;
    }
    // 对于 AOP 的基础支撑类,或者指定不需要被代理的类,设置为不进行代理
    if (this.isInfrastructureClass(bean.getClass()) || this.shouldSkip(bean.getClass(), beanName)) {
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

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

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

上述方法主要的工作是对 bean 实例进行筛选,过滤掉那些已经增强过的、支持 AOP 基础运行的,以及指定不需要被代理的 bean 实例。对于剩下的 bean 实例来说,首先会调用 AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean 方法获取适用于当前 bean 的增强器(Advisor),并基于这些增强器调用 AbstractAutoProxyCreator#createProxy 方法为当前 bean 创建增强后的代理对象。

筛选适用于 bean 的增强器

我们首先来看一下筛选适用于当前 bean 的合格增强器的过程,实现位于 AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean 方法中:

protected Object[] getAdvicesAndAdvisorsForBean(
        Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
    // 获取适用于当前 bean 的 Advisor
    List<Advisor> advisors = this.findEligibleAdvisors(beanClass, beanName);
    // 没有合格的 Advisor,不进行代理
    if (advisors.isEmpty()) {
        return DO_NOT_PROXY; // null
    }
    return advisors.toArray();
}

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
    // 获取所有候选的 Advisor(包括注解的、XML 中配置的)
    List<Advisor> candidateAdvisors = this.findCandidateAdvisors();
    // 从所有 Advisor 中寻找适用于当前 bean 的 Advisor
    List<Advisor> eligibleAdvisors = this.findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
    // 如果 Advisor 不为空,则在最前面追加一个 ExposeInvocationInterceptor
    this.extendAdvisors(eligibleAdvisors);
    // 对 Advisor 进行排序
    if (!eligibleAdvisors.isEmpty()) {
        eligibleAdvisors = this.sortAdvisors(eligibleAdvisors);
    }
    return eligibleAdvisors;
}

整个方法的执行流程很简单,获取所有的候选增强器,并从中找出适用于当前 bean 的增强器。首先来看获取所有候选增强器的过程,实现位于 AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors 方法中:

protected List<Advisor> findCandidateAdvisors() {
    // 调用父类的 findCandidateAdvisors 方法,兼容父类查找 Advisor 的规则
    List<Advisor> advisors = super.findCandidateAdvisors();
    // 获取所有注解定义的 Advisor
    if (this.aspectJAdvisorsBuilder != null) {
        advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
    }
    return advisors;
}

方法首先调用了父类的实现,这主要是为了兼容父类查找候选增强器的规则,例如我们的示例中使用的是注解方式定义的增强,但是父类却是基于 XML 配置的方式查找增强器,这里的兼容能够让我们在以注解方式编程时兼容其它以 XML 配置的方式定义的增强。下面还是将主要精力放在解析注解式增强定义上,该过程位于 BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors 方法中。不过该方法实现比较冗长,但是逻辑却很清晰,所以这里主要概括一下其执行流程:

  1. 获取所有类型 bean 实例对应的 beanName 集合;
  2. 过滤不是切面类型的 bean 对应的 beanName,即没有被 @Aspect 注解,或包含以 ajc$ 开头的字段,同时支持覆盖 BeanFactoryAspectJAdvisorsBuilder#isEligibleBean 方法扩展过滤规则;
  3. 对于切面 bean 类型,获取 bean 中定义的所有切点,并为每个切点生成对应的增强器;
  4. 缓存解析得到的增强器,避免重复解析。

上述流程中我们重点看一下过程 3,实现位于 ReflectiveAspectJAdvisorFactory#getAdvisors 方法中:

public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
    // 获取切面 aspect 对应的 class 和 beanName
    Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
    String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
    // 校验切面定义的合法性
    this.validate(aspectClass);

    // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator
    // so that it will only instantiate once.
    MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
            new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

    List<Advisor> advisors = new ArrayList<>();

    // 1. 遍历处理切面中除被 @Pointcut 注解以外的方法
    for (Method method : this.getAdvisorMethods(aspectClass)) {
        Advisor advisor = this.getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
        if (advisor != null) {
            advisors.add(advisor);
        }
    }

    // 2. 如果增强器不为空,同时又配置了增强延迟初始化,则需要追加实例化增强器 SyntheticInstantiationAdvisor
    if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
        Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
        advisors.add(0, instantiationAdvisor);
    }

    // 3. 获取所有引介增强定义
    for (Field field : aspectClass.getDeclaredFields()) {
        // 创建引介增强器 DeclareParentsAdvisor
        Advisor advisor = this.getDeclareParentsAdvisor(field);
        if (advisor != null) {
            advisors.add(advisor);
        }
    }

    return advisors;
}

上述实现的整体执行流程如代码注释。拿到一个切面定义,Spring 首先会遍历获取切面中的增强方法,即被 @Around@Before@After@AfterReturning,以及 @AfterThrowing 注解的方法,并调用 ReflectiveAspectJAdvisorFactory#getAdvisor 方法为每一个增强方法生成对应的增强器:

public Advisor getAdvisor(Method candidateAdviceMethod,
                          MetadataAwareAspectInstanceFactory aspectInstanceFactory,
                          int declarationOrderInAspect,
                          String aspectName) {

    // 校验切面类定义的合法性
    this.validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());

    // 获取注解配置的切点信息,封装成 AspectJExpressionPointcut 对象
    AspectJExpressionPointcut expressionPointcut = this.getPointcut(
            candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
    if (expressionPointcut == null) {
        return null;
    }

    // 依据切点信息生成对应的增强器
    return new InstantiationModelAwarePointcutAdvisorImpl(
            expressionPointcut, candidateAdviceMethod, this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}

上述实现首先对当前切面定义执行合法性校验,如果切面配置合法则获取目标方法上的切点注解定义,并封装成 AspectJExpressionPointcut 对象。该过程位于 ReflectiveAspectJAdvisorFactory#getPointcut 方法中,实现比较简单。

拿到切点注解定义之后,方法会依据切点的配置信息使用 InstantiationModelAwarePointcutAdvisorImpl 实现类创建对应的增强器。类 InstantiationModelAwarePointcutAdvisorImpl 的实例化过程除了初始化了一些基本属性之外,主要是调用了 InstantiationModelAwarePointcutAdvisorImpl#instantiateAdvice 方法,依据增强类型对增强器实施相应的初始化操作:

private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) {
    Advice advice = this.aspectJAdvisorFactory.getAdvice(
            this.aspectJAdviceMethod, pointcut, this.aspectInstanceFactory, this.declarationOrder, this.aspectName);
    return (advice != null ? advice : EMPTY_ADVICE);
}

// org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvice
public Advice getAdvice(Method candidateAdviceMethod,
                        AspectJExpressionPointcut expressionPointcut,
                        MetadataAwareAspectInstanceFactory aspectInstanceFactory,
                        int declarationOrder,
                        String aspectName) {

    // 获取切面 class 对象,并校验切面定义
    Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
    this.validate(candidateAspectClass);

    // 获取方法的切点注解定义
    AspectJAnnotation<?> aspectJAnnotation =
            AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
    if (aspectJAnnotation == null) {
        return null;
    }

    // If we get here, we know we have an AspectJ method.
    // Check that it's an AspectJ-annotated class
    if (!this.isAspect(candidateAspectClass)) {
        throw new AopConfigException("Advice must be declared inside an aspect type: " +
                "Offending method '" + candidateAdviceMethod + "' in class [" + candidateAspectClass.getName() + "]");
    }

    AbstractAspectJAdvice springAdvice;

    // 依据切点注解类型使用对应的增强类进行封装
    switch (aspectJAnnotation.getAnnotationType()) {
        // @Pointcut
        case AtPointcut:
            if (logger.isDebugEnabled()) {
                logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
            }
            return null;
        // @Around
        case AtAround:
            springAdvice = new AspectJAroundAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
            break;
        // @Before
        case AtBefore:
            springAdvice = new AspectJMethodBeforeAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
            break;
        // @After
        case AtAfter:
            springAdvice = new AspectJAfterAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
            break;
        // @AfterReturning
        case AtAfterReturning:
            springAdvice = new AspectJAfterReturningAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
            AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
            if (StringUtils.hasText(afterReturningAnnotation.returning())) {
                springAdvice.setReturningName(afterReturningAnnotation.returning());
            }
            break;
        // @AfterThrowing
        case AtAfterThrowing:
            springAdvice = new AspectJAfterThrowingAdvice(
                    candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
            AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
            if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
                springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
            }
            break;
        default:
            throw new UnsupportedOperationException("Unsupported advice type on method: " + candidateAdviceMethod);
    }

    // Now to configure the advice...
    springAdvice.setAspectName(aspectName);
    springAdvice.setDeclarationOrder(declarationOrder);
    String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
    if (argNames != null) {
        springAdvice.setArgumentNamesFromStringArray(argNames);
    }
    springAdvice.calculateArgumentBindings();

    return springAdvice;
}

方法的整体执行流程如代码注释,逻辑比较清晰,Spring 会依据具体的增强注解类型,选择相应的增强类对切点定义进行封装。这里我们以 @Before 为例说明一下增强的执行流程,AspectJMethodBeforeAdvice 增强类关联注册的处理器是 MethodBeforeAdviceInterceptor,当我们调用一个被前置增强的目标方法时,MethodBeforeAdviceInterceptor#invoke 方法会被触发:

public Object invoke(MethodInvocation mi) throws Throwable {
    // 执行增强方法
    this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
    // 执行目标方法
    return mi.proceed();
}

这里执行的增强方法就对应着 AspectJMethodBeforeAdvice#before 方法,该方法会依据切点配置将相应的参数绑定传递给我们自定义的增强方法,并最终通过反射调用触发执行。

上面分析了普通方法级别增强的处理过程,对于另外一类增强(引介增强),方法 ReflectiveAspectJAdvisorFactory#getAdvisors 则使用专门的 DeclareParentsAdvisor 类创建对应的增强器:

// 3. 获取所有引介增强定义
for (Field field : aspectClass.getDeclaredFields()) {
    // 创建引介增强器
    Advisor advisor = this.getDeclareParentsAdvisor(field);
    if (advisor != null) {
        advisors.add(advisor);
    }
}

private Advisor getDeclareParentsAdvisor(Field introductionField) {
    // 获取 @DeclareParents 注解定义
    DeclareParents declareParents = introductionField.getAnnotation(DeclareParents.class);
    if (declareParents == null) {
        return null;
    }

    // 没有指定默认的接口实现类
    if (DeclareParents.class == declareParents.defaultImpl()) {
        throw new IllegalStateException("'defaultImpl' attribute must be set on DeclareParents");
    }

    // 使用 DeclareParentsAdvisor 类型创建对应的引介增强器
    return new DeclareParentsAdvisor(
            introductionField.getType(), declareParents.value(), declareParents.defaultImpl());
}

对于引介增强来说,Spring 会注入 DelegatePerTargetObjectIntroductionInterceptor 处理器对其进行专门的处理,思想上与前面分析前置增强大同小异,这里不再展开。

继续回到 AbstractAdvisorAutoProxyCreator#findEligibleAdvisors 方法,上面的过程我们分析了获取所有类型增强器的过程,但是这些增强器不一定都适用于当前 bean 实例,我们需要依据切点配置信息对其进行筛选。这一过程位于 AbstractAdvisorAutoProxyCreator#findAdvisorsThatCanApply 方法中:

protected List<Advisor> findAdvisorsThatCanApply(
        List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {
    ProxyCreationContext.setCurrentProxiedBeanName(beanName);
    try {
        return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
    } finally {
        ProxyCreationContext.setCurrentProxiedBeanName(null);
    }
}

// org.springframework.aop.support.AopUtils#findAdvisorsThatCanApply
public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
    // 没有候选的增强器,直接返回
    if (candidateAdvisors.isEmpty()) {
        return candidateAdvisors;
    }
    List<Advisor> eligibleAdvisors = new ArrayList<>();

    // 1. 筛选引介增强器
    for (Advisor candidate : candidateAdvisors) {
        if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
            eligibleAdvisors.add(candidate);
        }
    }
    // 表示是否含有引介增强
    boolean hasIntroductions = !eligibleAdvisors.isEmpty();

    // 2. 筛选其它类型的增强器
    for (Advisor candidate : candidateAdvisors) {
        // 引介增强已经处理过,这里直接跳过
        if (candidate instanceof IntroductionAdvisor) {
            // already processed
            continue;
        }
        // 筛选其它类型的增强器
        if (canApply(candidate, clazz, hasIntroductions)) {
            eligibleAdvisors.add(candidate);
        }
    }
    return eligibleAdvisors;
}

方法首先会使用类过滤器(ClassFilter)筛选引介增强器,除了我们手动注册的类过滤器外,这里默认还会使用 TypePatternClassFilter 类过滤器执行过滤操作。然后,方法会过滤筛选其它类型的增强器,这里除了使用类过滤器外,考虑方法级别增强的定义形式,还会使用方法匹配器(MethodMatcher)进行筛选。如果增强器适用于当前 bean 类型,则将其加入到集合中用于下一步为当前 bean 创建增强代理对象。如果没有任何一个增强器适用于当前 bean 类型,则方法 AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean 最终会返回值为 null 的 DO_NOT_PROXY 数组对象,表示当前 bean 不需要被增强。

为 bean 创建增强代理对象

完成了对于当前 bean 增强器的筛选,接下来我们继续回到 AbstractAutoProxyCreator#wrapIfNecessary 方法,看一下基于前面筛选出的增强器为当前 bean 创建增强代理对象的过程,实现位于 AbstractAutoProxyCreator#createProxy 方法中:

protected Object createProxy(Class<?> beanClass,
                             @Nullable String beanName,
                             @Nullable Object[] specificInterceptors,
                             TargetSource targetSource) {

    if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
        AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
    }

    // ProxyFactory 用于为目标 bean 实例创建代理对象
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.copyFrom(this);

    // proxy-target-class = false,表示使用 JDK 原生动态代理
    if (!proxyFactory.isProxyTargetClass()) {
        // 检测当前 bean 是否应该基于类而非接口生成代理对象,即包含 preserveTargetClass=true 属性
        if (this.shouldProxyTargetClass(beanClass, beanName)) {
            proxyFactory.setProxyTargetClass(true);
        }
        // 如果是基于接口生成代理,则添加需要代理的接口到 ProxyFactory 中(除内置 callback 接口、语言内在接口,以及标记接口)
        else {
            this.evaluateProxyInterfaces(beanClass, proxyFactory);
        }
    }

    // 将拦截器封装成 Advisor 对象
    Advisor[] advisors = this.buildAdvisors(beanName, specificInterceptors);
    proxyFactory.addAdvisors(advisors);
    proxyFactory.setTargetSource(targetSource);
    // 模板方法,定制代理工厂
    this.customizeProxyFactory(proxyFactory);

    // 设置代理工厂被配置之后是否还允许修改,默认为 false,表示不允许修改
    proxyFactory.setFrozen(this.freezeProxy);
    if (this.advisorsPreFiltered()) {
        proxyFactory.setPreFiltered(true);
    }

    // 基于 ProxyFactory 创建代理类
    return proxyFactory.getProxy(this.getProxyClassLoader());
}

方法的执行流程如代码注释。下面我们主要分析将拦截器封装成 Advisor 对象的过程,以及基于 ProxyFactory 创建增强代理对象的过程。

Spring 定义了非常多的拦截器、增强器,以及增强方法等,这里通过 AbstractAutoProxyCreator#buildAdvisors 方法统一将他们封装成 Advisor 对象,从而简化代理的创建过程。封装的核心步骤由 DefaultAdvisorAdapterRegistry#wrap 方法实现:

public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
    // 已经是 Advisor,则无需多做处理
    if (adviceObject instanceof Advisor) {
        return (Advisor) adviceObject;
    }
    // 要求必须是 Advice 类型
    if (!(adviceObject instanceof Advice)) {
        throw new UnknownAdviceTypeException(adviceObject);
    }
    Advice advice = (Advice) adviceObject;
    // 如果是 MethodInterceptor,则直接使用 DefaultPointcutAdvisor 进行包装
    if (advice instanceof MethodInterceptor) {
        // So well-known it doesn't even need an adapter.
        return new DefaultPointcutAdvisor(advice);
    }
    // 否则遍历注册的适配器,如果存在关联的适配器则使用 DefaultPointcutAdvisor 进行包装
    for (AdvisorAdapter adapter : this.adapters) {
        // Check that it is supported.
        if (adapter.supportsAdvice(advice)) {
            return new DefaultPointcutAdvisor(advice);
        }
    }
    throw new UnknownAdviceTypeException(advice);
}

接下来我们重点分析一下通过代理工厂 ProxyFactory 创建增强代理对象的过程,实现位于 ProxyFactory#getProxy 方法中:

public Object getProxy(@Nullable ClassLoader classLoader) {
    return this.createAopProxy() // 1. 创建 AOP 代理
            .getProxy(classLoader); // 2. 基于 AOP 代理创建目标类的增强代理对象
}

该方法的执行过程可以拆分成两个步骤:

  1. 创建 AOP 代理,Spring 默认提供了两种 AOP 代理实现,即 java 原生代理和 CGLib 代理;
  2. 基于 AOP 代理创建目标类的增强代理对象。

我们首先来看一下步骤 1 的实现,位于 ProxyCreatorSupport#createAopProxy 方法中:

protected final synchronized AopProxy createAopProxy() {
    if (!this.active) {
        this.activate();
    }
    return this.getAopProxyFactory().createAopProxy(this);
}

// org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (config.isOptimize() // 需要对代理策略进行优化
            || config.isProxyTargetClass() // // 指定使用 CGLib 生成代理对象
            || this.hasNoUserSuppliedProxyInterfaces(config)) // 当前类没有接口定义,不得不使用 CGLib
    {
        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);
    }
    // 使用 JDK 原生动态代理
    else {
        return new JdkDynamicAopProxy(config);
    }
}

这部分代码清晰说明了 Spring 在生成代理对象时如何在 java 原生代理和 CGLib 代理之间进行选择,可以概括如下:

  1. 如果目标类实现了接口,则 Spring 默认会使用 java 原生代理。
  2. 如果目标类未实现接口,则 Spring 会使用 CGLib 生成代理。
  3. 如果目标类实现了接口,但是在配置时指定了 proxy-target-class=true,则使用 CGLib 生成代理。

下面分别对基于 java 原生代理和 CGLib 代理生成增强代理对象的过程进行分析。

基于 java 原生代理创建增强代理对象

首先来看一下基于 java 原生代理生成增强代理对象的过程,位于 JdkDynamicAopProxy 类中。Java 原生代理要求代理类实现 InvocationHandler 接口,并在 InvocationHandler#invoke 方法中实现代理增强逻辑。JdkDynamicAopProxy 正好实现了该接口,对应的 JdkDynamicAopProxy#invoke 方法实现如下:

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

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

    try {
        // 当前是 equals 方法,但是被代理类接口中未定义 equals 方法
        if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
            return this.equals(args[0]);
        }
        // 当前是 hashCode 方法,但是被代理类接口中未定义 hashCode 方法
        else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
            return this.hashCode();
        }
        // 如果是 DecoratingProxy 中定义的方法(即 DecoratingProxy#getDecoratedClass),直接返回目标类对象
        else if (method.getDeclaringClass() == DecoratingProxy.class) {
            return AopProxyUtils.ultimateTargetClass(this.advised);
        } else if (!this.advised.opaque // 允许被转换成 Advised 类型
                && method.getDeclaringClass().isInterface() // 接口类型
                && method.getDeclaringClass().isAssignableFrom(Advised.class)) // 方法所在类是 Advised 类及其父类
        {
            // 直接反射调用该方法
            return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
        }

        // 结果值
        Object retVal;

        // 指定内部间调用也需要代理
        if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }

        // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool.
        target = targetSource.getTarget();
        Class<?> targetClass = (target != null ? target.getClass() : null);

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

        // Check whether we have any advice. If we don't, we can fallback on direct
        // reflective invocation of the target, and avoid creating a MethodInvocation.
        // 拦截器链为空,则直接反射调用增强方法
        if (chain.isEmpty()) {
            // We can skip creating a MethodInvocation: just invoke the target directly
            // Note that the final invoker must be an InvokerInterceptor so we know it does
            // nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
        }
        // 否则需要创建对应的 MethodInvocation,以链式调用拦截器方法和增强方法
        else {
            MethodInvocation invocation =
                    new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
            // Proceed to the joinpoint through the interceptor chain.
            retVal = invocation.proceed();
        }

        // 处理返回值
        Class<?> returnType = method.getReturnType();
        if (retVal != null && retVal == target &&
                returnType != Object.class && returnType.isInstance(proxy) &&
                !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
            // Special case: it returned "this" and the return type of the method is type-compatible.
            // Note that we can't help if the target sets a reference to itself in another returned object.
            retVal = proxy;
        } else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
            throw new AopInvocationException(
                    "Null return value from advice does not match primitive return type for: " + method);
        }
        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. 特殊处理 Object#equalsObject#hashCodeDecoratingProxy#getDecoratedClass,以及 Advised 类及其父类中定义的方法;
  2. 如果配置了 expose-proxy 属性,则记录当前代理对象,以备在内部间调用时实施增强;
  3. 获取当前方法的拦截器链;
  4. 如果没有拦截器定义,则直接反射调用增强方法,否则先逐一执行拦截器方法,最后再应用增强方法;
  5. 处理返回值。

重点来看一下步骤 4 中应用拦截器方法的实现,位于 ReflectiveMethodInvocation#proceed 方法中:

public Object proceed() throws Throwable {
    // 如果所有的增强都执行完成,则执行增强方法
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return this.invokeJoinpoint();
    }

    // 获取下一个需要执行的拦截器
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    // 动态拦截器,执行动态方法匹配
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        // Evaluate dynamic method matcher here: static part will already have been evaluated and found to match.
        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 this.proceed();
        }
    }
    // 静态拦截器,直接应用拦截方法
    else {
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

拦截器方法的执行流程如上述代码注释,是一个递归调用的过程,并在最后应用增强方法。

完成了对于 AOP 代理对象 JdkDynamicAopProxy 的创建,最后来看一下获取该对象的过程,实现位于 JdkDynamicAopProxy#getProxy 方法中:

public Object getProxy(@Nullable ClassLoader classLoader) {
    // 获取需要被代理的接口集合
    Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
    // 检测是否在被代理接口中声明了 equals 和 hashCode 方法
    this.findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
    // 基于 java 原生代理生成代理对象
    return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

这里的逻辑也就是 java 原生代理的模板代码,如果对 java 代理比较熟悉的话,应该不难理解。

基于 CGLib 代理创建增强代理对象

基于 CGLib 代理生成增强代理对象的过程位于 ObjenesisCglibAopProxy 类中,该类继承自 CglibAopProxy 类。获取 CGLib 代理类对象的方法定义在 CglibAopProxy 中,即 CglibAopProxy#getProxy 方法。该方法基于 CGLib 的 Enhancer 类创建代理对象,属于 CGLib 的标准使用模式,因为有多个 callback 实现,所以这里使用了 CallbackFilter 模式,依据场景选择并应用对应的 callback 拦截器。

我们重点关注 callback 的实现,位于 CglibAopProxy#getCallbacks 方法中。受制于 CGLib 在执行时一次只允许应用一个 callback 的约束,所以该方法依据参数配置实现了一组 callback,以覆盖不同的场景。核心的 AOP callback 实现是 DynamicAdvisedInterceptor 类,它实现了 MethodInterceptor 接口,对应的 DynamicAdvisedInterceptor#intercept 方法实现如下:

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 {
        // 指定内部间调用也需要代理
        if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }
        // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
        target = targetSource.getTarget();
        Class<?> targetClass = (target != null ? target.getClass() : null);

        // 获取当前方法的拦截器链
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
        // 结果值
        Object retVal;
        // Check whether we only have one InvokerInterceptor:
        // that is, no real advice, but just reflective invocation of the target.
        // 拦截器链为空,则直接反射调用增强方法
        if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
            // We can skip creating a MethodInvocation: just invoke the target directly.
            // Note that the final invoker must be an InvokerInterceptor, so we know
            // it does nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            retVal = methodProxy.invoke(target, argsToUse);
        }
        // 否则需要创建对应的 MethodInvocation,以链式调用拦截器方法和增强方法
        else {
            retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
        }
        // 处理返回值
        retVal = processReturnType(proxy, target, method, retVal);
        return retVal;
    } finally {
        if (target != null && !targetSource.isStatic()) {
            targetSource.releaseTarget(target);
        }
        if (setProxyContext) {
            // Restore old proxy.
            AopContext.setCurrentProxy(oldProxy);
        }
    }
}

可以看出上述方法在实现流程上与前面介绍的 JdkDynamicAopProxy#invoke 方法是一致的,只是这里是基于 CGLib 实现而已。

总结

最后我们对 Spring AOP 的运行机制进行一个总结。Spring AOP 的实现本质上是一个动态代理的过程,Spring 引入了 java 原生代理和 CGLib 代理,并依据场景选择基于哪种代理机制对目标对象进行增强。由前面对于 Spring IoC 实现的分析可以了解到,Spring 容器在完成对 bean 对象的创建之后会执行初始化操作,而 AOP 初始化的过程就发生在 bean 的后置初始化阶段,整体流程可以概括为:

  1. 从容器中获取所有的切面定义;
  2. 筛选适用于当前 bean 的增强器集合;
  3. 依据增强器集合基于动态代理机制生成相应的增强代理对象。

当我们在调用一个被增强的方法时,相应的拦截器会依据连接点的方位在适当的位置触发对应的增强定义,从而最终实现 AOP 中定义的各类增强语义。

© 著作权归作者所有

zhenchao
粉丝 126
博文 18
码字总数 99610
作品 0
海淀
高级程序员
私信 提问
加载中

评论(3)

嘴角轻扬30
嘴角轻扬30
学习了,哈哈哈
yong9981
yong9981
Spring的源码分析起来太累了,如果只是想了解AOP原理, 可以看看我写的jBeanBox,它的源码不到3000行。另外AcpectJ这个比较复杂,通常用AOP联盟标准更简单和规范一些,例如Guice也是实现了AOP联盟标准。
写给三月
写给三月
👏👍👍👍👍👍👍👍
深入聊一聊 Spring AOP 实现机制!

作者 | 张书康 责编 | 郭 芮 AOP(Aspect-Oriented Programming,即面向切面编程。Spring Aop 在 Spring框架中的地位举足轻重,主要用于实现事务、缓存、安全等功能。本篇主要是对源码进行深...

CSDN资讯
2019/01/26
0
0
Spring AOP 功能使用详解

相关文章 Spring 中 bean 注册的源码解析 Spring bean 创建过程源码解析 Spring 的 getBean 方法源码解析 前言 AOP 既熟悉又陌生,了解过 Spring 人的都知道 AOP 的概念,即面向切面编程,可...

TSMYK
2018/12/25
887
2
spring-AOP原理与应用

什么是AOP Spring是解决实际开发中的一些问题: * AOP解决OOP中遇到的一些问题.是OOP的延续和扩展. AOP作用 对程序进行增强:不修改源码的情况下. * AOP可以进行权限校验,日志记录,性能监控,事...

叫我北北
2018/06/29
0
0
AOP切面实现原理以及多个切面切同一个地方时的优先级讲解

此博文的编写,源于前段时间的惨痛面试经历。刚好近几天尘埃落定、手头事少,遂总结一二,与各位道友分享,欢迎吐槽指正。今年年初的这段面试经历,已于之前的博文中 整理发出(https://www...

osc_gu9d45li
2019/04/13
12
0
Spring入门(二)— IOC注解、Spring测试、AOP入门

一、Spring整合Servlet背后的细节 1. 为什么要在web.xml中配置listener <listener> </listener> 配置listener主要是为了捕获项目发布 | 服务器启动的契机 ,为了解析xml , 创建工厂。 这个l...

osc_jjc36t9p
2018/02/19
10
0

没有更多内容

加载失败,请刷新页面

加载更多

Windows 10 中安装 Anaconda 3

首先通过下面链接地址下载 Anaconda 的个人版本。 https://www.anaconda.com/products/individual 从上面下载的地址中,选择你需要的版本,目前 Windows 应该基本上都是 64 位的了。 在你下载...

honeymoose
41分钟前
19
0
如何禁用textarea的resizable属性? - How do I disable the resizable property of a textarea?

问题: I want to disable the resizable property of a textarea . 我想禁用textarea的resizable属性。 Currently, I can resize a textarea by clicking on the bottom right corner of t......

技术盛宴
41分钟前
25
0
即时通信E聊SDK简介(1)

2.简介: E聊SDK是一套适用于PC端, 移动端的即时通讯解决方案,源代码开放。E聊整合了即时通讯的基础能力,使用E聊,您可以让您的应用快速接入即时聊天的功能。E聊现已适配PC Web, 移动Web, ...

E聊
53分钟前
15
0
多个知乎账号一起登陆,同时管理运营的神器!

随着互联网生态的优化,从2016年开始,信息内容产业相当有关注度,其和粉丝互动起来很方便、流量大到惊人、可长远发展等等优势,迅速聚集了无数企业和个人,为了有非常全面的播放数据,大家通...

易媒助手
56分钟前
22
0
403禁止vs 401未经授权的HTTP响应 - 403 Forbidden vs 401 Unauthorized HTTP responses

问题: For a web page that exists, but for which a user does not have sufficient privileges (they are not logged in or do not belong to the proper user group), what is the prope......

fyin1314
今天
19
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部