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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介:

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

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 使用的一个简单示例,并没有覆盖所有的特性。对于 Spring AOP 特性的介绍不属于本文的范畴,不过我们还是会在下面分析源码的过程中进行针对性的介绍。

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

// 注册 标签及其解析器
this.registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
AspectJAutoProxyBeanDefinitionParser 类是标签  的解析器,该类实现了 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;

}
该方法做了两件事情:注册标签解析器和处理  子标签。本文我们重点来看标签解析器的注册过程,即 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,这一步主要是用来解析标签  的 proxy-target-class 和 expose-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-class 和 expose-proxy 属性添加到对应 BeanDefinition 的属性列表中。那么 AnnotationAwareAspectJAutoProxyCreator 到底是来做什么的呢?我们先来看一下它的类继承关系图:

从类继承关系图中可以看到该类实现了 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 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 findCandidateAdvisors() {

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

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

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

public List 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 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 findAdvisorsThatCanApply(List 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 代理创建目标类的增强代理对象

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

创建 AOP 代理,Spring 默认提供了两种 AOP 代理实现,即 java 原生代理和 CGLib 代理;
基于 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 代理之间进行选择,可以概括如下:

如果目标类实现了接口,则 Spring 默认会使用 java 原生代理。
如果目标类未实现接口,则 Spring 会使用 CGLib 生成代理。
如果目标类实现了接口,但是在配置时指定了 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);
    }
}

}
由上述方法实现,我们可以概括出整个增强代理的执行过程,如下:

特殊处理 Object#equals、Object#hashCode、DecoratingProxy#getDecoratedClass,以及 Advised 类及其父类中定义的方法;
如果配置了 expose-proxy 属性,则记录当前代理对象,以备在内部间调用时实施增强;
获取当前方法的拦截器链;
如果没有拦截器定义,则直接反射调用增强方法,否则先逐一执行拦截器方法,最后再应用增强方法;
处理返回值。
重点来看一下步骤 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 的后置初始化阶段,整体流程可以概括为:

从容器中获取所有的切面定义;
筛选适用于当前 bean 的增强器集合;
依据增强器集合基于动态代理机制生成相应的增强代理对象。
当我们在调用一个被增强的方法时,相应的拦截器会依据连接点的方位在适当的位置触发对应的增强定义,从而最终实现 AOP 中定义的各类增强语义。

原文地址https://my.oschina.net/wangzhenchao/blog/4279608

相关文章
|
4天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
20天前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
10天前
|
前端开发 Java 开发者
Spring生态学习路径与源码深度探讨
【11月更文挑战第13天】Spring框架作为Java企业级开发中的核心框架,其丰富的生态系统和强大的功能吸引了无数开发者的关注。学习Spring生态不仅仅是掌握Spring Framework本身,更需要深入理解其周边组件和工具,以及源码的底层实现逻辑。本文将从Spring生态的学习路径入手,详细探讨如何系统地学习Spring,并深入解析各个重点的底层实现逻辑。
36 9
|
6天前
|
存储 消息中间件 算法
深入探索操作系统的心脏——内核机制解析
本文旨在揭示操作系统核心——内核的工作原理,通过剖析其关键组件与机制,为读者提供一个清晰的内核结构图景。不同于常规摘要的概述性内容,本文摘要将直接聚焦于内核的核心概念、主要功能以及其在系统管理中扮演的角色,旨在激发读者对操作系统深层次运作原理的兴趣与理解。
|
18天前
|
存储 缓存 安全
🌟Java零基础:深入解析Java序列化机制
【10月更文挑战第20天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
22 3
|
17天前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
33 1
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
1月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
163 2
|
9天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
20 2
 SpringBoot入门(7)- 配置热部署devtools工具
|
5天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
17 2

推荐镜像

更多