自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Spring AOP 中的代理對(duì)象是怎么創(chuàng)建出來的?

開發(fā) 前端
首先實(shí)例化就是通過反射,先把 Bean 的實(shí)例創(chuàng)建出來;接下來屬性賦值就是給創(chuàng)建出來的 Bean 的各個(gè)屬性賦值;接下來的初始化就是給 Bean 應(yīng)用上各種需要的后置處理器;最后則是銷毀。

1. AOP 用法

先來一個(gè)簡單的案例,小伙伴們先回顧一下 AOP,假設(shè)我有如下類:

@Service
public class UserService {

    public void hello() {
        System.out.println("hello javaboy");
    }
}

然后我寫一個(gè)切面,攔截 UserService 中的方法:

@Component
@Aspect
@EnableAspectJAutoProxy
public class LogAspect {

    @Before("execution(* org.javaboy.bean.aop.UserService.*(..))")
    public void before(JoinPoint jp) {
        String name = jp.getSignature().getName();
        System.out.println(name+" 方法開始執(zhí)行了...");
    }
}

最后,我們看一下從 Spring 容器中獲取到的 UserService 對(duì)象:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml");
UserService us = ctx.getBean(UserService.class);
System.out.println("us.getClass() = " + us.getClass());

打印結(jié)果如下:

圖片圖片

可以看到,獲取到的 UserService 是一個(gè)代理對(duì)象。

其他各種類型的通知我這里就不說了,不熟悉的小伙伴可以在公眾號(hào)【江南一點(diǎn)雨】后臺(tái)回復(fù) ssm,有松哥錄制的免費(fèi)入門視頻。

2. 原理分析

那么注入到 Spring 容器中的 UserService,為什么在獲取的時(shí)候變成了一個(gè)代理對(duì)象,而不是原本的 UserService 了呢?

整體上來說,我們可以將 Spring Bean 的生命周期分為四個(gè)階段,分別是:

實(shí)例化。

屬性賦值。

初始化。

銷毀。

如下圖:

圖片圖片

首先實(shí)例化就是通過反射,先把 Bean 的實(shí)例創(chuàng)建出來;接下來屬性賦值就是給創(chuàng)建出來的 Bean 的各個(gè)屬性賦值;接下來的初始化就是給 Bean 應(yīng)用上各種需要的后置處理器;最后則是銷毀。

2.1 doCreateBean

AOP 代理對(duì)象的創(chuàng)建是在初始化這個(gè)過程中完成的,所以今天我們就從初始化這里開始看起。

AbstractAutowireCapableBeanFactory#doCreateBean:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
  throws BeanCreationException {
    //...
 try {
  populateBean(beanName, mbd, instanceWrapper);
  exposedObject = initializeBean(beanName, exposedObject, mbd);
 }
 //...
 return exposedObject;
}

小伙伴們看到,這里有一個(gè) initializeBean 方法,在這個(gè)方法中會(huì)對(duì) Bean 執(zhí)行各種后置處理器:

protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
 invokeAwareMethods(beanName, bean);
 Object wrappedBean = bean;
 if (mbd == null || !mbd.isSynthetic()) {
  wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
 }
 try {
  invokeInitMethods(beanName, wrappedBean, mbd);
 }
 catch (Throwable ex) {
  throw new BeanCreationException(
    (mbd != null ? mbd.getResourceDescription() : null), beanName, ex.getMessage(), ex);
 }
 if (mbd == null || !mbd.isSynthetic()) {
  wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
 }
 return wrappedBean;
}

這里一共是執(zhí)行了四個(gè)方法,也都是非常常見的 Bean 初始化方法:

  1. invokeAwareMethods:執(zhí)行 Aware 接口下的 Bean。
  2. applyBeanPostProcessorsBeforeInitialization:執(zhí)行 BeanPostProcessor 中的前置方法。
  3. invokeInitMethods:執(zhí)行 Bean 的初始化方法 init。
  4. applyBeanPostProcessorsAfterInitialization:執(zhí)行 BeanPostProcessor 中的后置方法。

1、3 這兩個(gè)方法很明顯跟 AOP 關(guān)系不大,我們自己平時(shí)創(chuàng)建的 AOP 對(duì)象基本上都是在 applyBeanPostProcessorsAfterInitialization 中進(jìn)行處理的,我們來看下這個(gè)方法:

@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
  throws BeansException {
 Object result = existingBean;
 for (BeanPostProcessor processor : getBeanPostProcessors()) {
  Object current = processor.postProcessAfterInitialization(result, beanName);
  if (current == null) {
   return result;
  }
  result = current;
 }
 return result;
}

小伙伴們看到,這里就是遍歷各種 BeanPostProcessor,并執(zhí)行其 postProcessAfterInitialization 方法,將執(zhí)行結(jié)果賦值給 result 并返回。

2.2 postProcessAfterInitialization

BeanPostProcessor 有一個(gè)實(shí)現(xiàn)類 AbstractAutoProxyCreator,在 AbstractAutoProxyCreator 的 postProcessAfterInitialization 方法中,進(jìn)行了 AOP 的處理:

@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;
 }
 // Create proxy if we have advice.
 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;
}

可以看到,首先會(huì)嘗試去緩存中獲取代理對(duì)象,如果緩存中沒有的話,則會(huì)調(diào)用 wrapIfNecessary 方法進(jìn)行 AOP 的創(chuàng)建。

正常來說,普通 AOP 的創(chuàng)建,前面三個(gè) if 的條件都是不滿足的。第一個(gè) if 是說 beanName 是否是一個(gè) targetSource,顯然我們這里不是;第二個(gè) if 是說這個(gè) Bean 是不是不需代理(結(jié)合上篇文章一起理解),我們這里顯然是需要代理的;第三個(gè) if 的作用我們也在上篇文章中和小伙伴們介紹過,這里就不再贅述了。

關(guān)于第二個(gè) if 我多說一句,如果這里進(jìn)來的是一個(gè)切面的 Bean,例如第一小節(jié)中的 LogAspect,這種 Bean 顯然是不需要代理的,所以會(huì)在第二個(gè)方法中直接返回,如果是其他普通的 Bean,則第二個(gè) if 并不會(huì)進(jìn)來。

所在在 wrapIfNecessary 中,最重要的方法實(shí)際上就是兩個(gè):getAdvicesAndAdvisorsForBean 和 createProxy,前者用來找出來所有跟當(dāng)前類匹配的切面,后者則用來創(chuàng)建代理對(duì)象。

2.3 getAdvicesAndAdvisorsForBean

這個(gè)方法,說白了,就是查找各種 Advice(通知/增強(qiáng)) 和 Advisor(切面)。來看下到底怎么找的:

AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean:

@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
  Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
 List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
 if (advisors.isEmpty()) {
  return DO_NOT_PROXY;
 }
 return advisors.toArray();
}

從這里可看到,這個(gè)方法主要就是調(diào)用 findEligibleAdvisors 去獲取到所有的切面,繼續(xù):

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
 List<Advisor> candidateAdvisors = findCandidateAdvisors();
 List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
 extendAdvisors(eligibleAdvisors);
 if (!eligibleAdvisors.isEmpty()) {
  eligibleAdvisors = sortAdvisors(eligibleAdvisors);
 }
 return eligibleAdvisors;
}

這里一共有三個(gè)主要方法:

  • findCandidateAdvisors:這個(gè)方法是查詢到所有候選的 Advisor,說白了,就是把項(xiàng)目啟動(dòng)時(shí)注冊(cè)到 Spring 容器中所有切面都找到,由于一個(gè) Aspect 中可能存在多個(gè) Advice,每個(gè) Advice 最終都能封裝為一個(gè) Advisor,所以在具體查找過程中,找到 Aspect Bean 之后,還需要遍歷 Bean 中的方法。
  • findAdvisorsThatCanApply:這個(gè)方法主要是從上個(gè)方法找到的所有切面中,根據(jù)切點(diǎn)過濾出來能夠應(yīng)用到當(dāng)前 Bean 的切面。
  • extendAdvisors:這個(gè)是添加一個(gè) DefaultPointcutAdvisor 切面進(jìn)來,這個(gè)切面使用的 Advice 是 ExposeInvocationInterceptor,ExposeInvocationInterceptor 的作用是用于暴露  MethodInvocation 對(duì)象到 ThreadLocal 中,如果其他地方需要使用當(dāng)前的 MethodInvocation 對(duì)象,直接通過調(diào)用 currentInvocation 方法取出即可。

接下來我們就來看一下這三個(gè)方法的具體實(shí)現(xiàn)。

2.3.1 findCandidateAdvisors

AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors

@Override
protected List<Advisor> findCandidateAdvisors() {
 List<Advisor> advisors = super.findCandidateAdvisors();
 if (this.aspectJAdvisorsBuilder != null) {
  advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
 }
 return advisors;
}

這個(gè)方法的關(guān)鍵在于通過 buildAspectJAdvisors 構(gòu)建出所有的切面,這個(gè)方法有點(diǎn)復(fù)雜:

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<>();
    String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
      this.beanFactory, Object.class, true, false);
    for (String beanName : beanNames) {
     if (!isEligibleBean(beanName)) {
      continue;
     }
     // We must be careful not to instantiate beans eagerly as in this case they
     // would be cached by the Spring container but would not have been weaved.
     Class<?> beanType = this.beanFactory.getType(beanName, false);
     if (beanType == null) {
      continue;
     }
     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);
       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);
      }
      else {
       // Per target or per this.
       if (this.beanFactory.isSingleton(beanName)) {
        throw new IllegalArgumentException("Bean with name '" + beanName +
          "' is a singleton, but aspect instantiation model is not singleton");
       }
       MetadataAwareAspectInstanceFactory factory =
         new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
       this.aspectFactoryCache.put(beanName, factory);
       advisors.addAll(this.advisorFactory.getAdvisors(factory));
      }
     }
    }
    this.aspectBeanNames = aspectNames;
    return advisors;
   }
  }
 }
 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);
   advisors.addAll(this.advisorFactory.getAdvisors(factory));
  }
 }
 return advisors;
}

這個(gè)方法第一次進(jìn)來的時(shí)候,aspectNames 變量是沒有值的,所以會(huì)先進(jìn)入到 if 分支中,給 aspectNames 和 aspectBeanNames 兩個(gè)變量賦值。

具體過程就是首先調(diào)用 BeanFactoryUtils.beanNamesForTypeIncludingAncestors 方法(不熟悉該方法的小伙伴參考 Spring 中的父子容器是咋回事?一文),去當(dāng)前容器以及當(dāng)前容器的父容器中,查找到所有的 beanName,將返回的數(shù)組賦值給 beanNames 變量,然后對(duì) beanNames 進(jìn)行遍歷。

遍歷時(shí),首先調(diào)用 isEligibleBean 方法,這個(gè)方法是檢查給定名稱的 Bean 是否符合自動(dòng)代理的條件的,這個(gè)細(xì)節(jié)我們就不看了,因?yàn)橐话闱闆r下,我們項(xiàng)目中的 AOP 都是自動(dòng)代理的。

接下來根據(jù) beanName,找到對(duì)應(yīng)的 bean 類型 beanType,然后調(diào)用 advisorFactory.isAspect 方法去判斷這個(gè) beanType 是否是一個(gè) Aspect,具體的判斷過程上篇文章講過了,小伙伴們可以參考。

如果當(dāng)前 beanName 對(duì)應(yīng)的 Bean 是一個(gè) Aspect,那么就把 beanName 添加到 aspectNames 集合中,并且把 beanName 和 beanType 封裝為一個(gè) AspectMetadata 對(duì)象。

接下來會(huì)去判斷 kind 是否為 SINGLETON,這個(gè)默認(rèn)都是 SINGLETON,所以這里會(huì)進(jìn)入到分支中,進(jìn)來之后,會(huì)調(diào)用 this.advisorFactory.getAdvisors 方法去 Aspect 中找到各種通知和切點(diǎn)并封裝成 Advisor 對(duì)象返回,由于一個(gè)切面中可能定義多個(gè)通知,所以最終返回的 Advisor 是一個(gè)集合,最后把找到的 Advisor 集合存入到 advisorsCache 緩存中。

后面方法的邏輯就很好懂了,從 advisorsCache 中找到某一個(gè) aspect 對(duì)應(yīng)的所有 Advisor,并將之存入到 advisors 集合中,然后返回集合。

這樣,我們就找到了所有的 Advisor。

2.3.2 findAdvisorsThatCanApply

接下來 findAdvisorsThatCanApply 方法主要是從眾多的 Advisor 中,找到能匹配上當(dāng)前 Bean 的 Advisor,小伙伴們知道,每一個(gè) Advisor 都包含一個(gè)切點(diǎn) Pointcut,不同的切點(diǎn)意味著不同的攔截規(guī)則,所以現(xiàn)在需要進(jìn)行匹配,檢查當(dāng)前類需要和哪個(gè) Advisor 匹配:

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

這里實(shí)際上就是調(diào)用了靜態(tài)方法 AopUtils.findAdvisorsThatCanApply 去查找匹配的 Advisor:

public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
 if (candidateAdvisors.isEmpty()) {
  return candidateAdvisors;
 }
 List<Advisor> eligibleAdvisors = new ArrayList<>();
 for (Advisor candidate : candidateAdvisors) {
  if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
   eligibleAdvisors.add(candidate);
  }
 }
 boolean hasIntroductions = !eligibleAdvisors.isEmpty();
 for (Advisor candidate : candidateAdvisors) {
  if (candidate instanceof IntroductionAdvisor) {
   // already processed
   continue;
  }
  if (canApply(candidate, clazz, hasIntroductions)) {
   eligibleAdvisors.add(candidate);
  }
 }
 return eligibleAdvisors;
}

這個(gè)方法中首先會(huì)去判斷 Advisor 的類型是否是 IntroductionAdvisor 類型,IntroductionAdvisor 類型的 Advisor 只能在類級(jí)別進(jìn)行攔截,靈活度不如 PointcutAdvisor,所以我們一般都不是 IntroductionAdvisor,因此這里最終會(huì)走入到最后一個(gè)分支中:

public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
 if (advisor instanceof IntroductionAdvisor ia) {
  return ia.getClassFilter().matches(targetClass);
 }
 else if (advisor instanceof PointcutAdvisor pca) {
  return canApply(pca.getPointcut(), targetClass, hasIntroductions);
 }
 else {
  // It doesn't have a pointcut so we assume it applies.
  return true;
 }
}

從這里小伙伴們就能看到,IntroductionAdvisor 類型的 Advisor 只需要調(diào)用 ClassFilter 過濾一下就行了,ClassFilter 松哥在前面的文章中已經(jīng)介紹過了(玩一玩編程式 AOP),小伙伴們看這里的匹配邏輯也是非常 easy!而 PointcutAdvisor 類型的 Advisor 則會(huì)繼續(xù)調(diào)用 canApply 方法進(jìn)行判斷:

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
 if (!pc.getClassFilter().matches(targetClass)) {
  return false;
 }
 MethodMatcher methodMatcher = pc.getMethodMatcher();
 if (methodMatcher == MethodMatcher.TRUE) {
  // No need to iterate the methods if we're matching any method anyway...
  return true;
 }
 IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
 if (methodMatcher instanceof IntroductionAwareMethodMatcher iamm) {
  introductionAwareMethodMatcher = iamm;
 }
 Set<Class<?>> classes = new LinkedHashSet<>();
 if (!Proxy.isProxyClass(targetClass)) {
  classes.add(ClassUtils.getUserClass(targetClass));
 }
 classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
 for (Class<?> clazz : classes) {
  Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
  for (Method method : methods) {
   if (introductionAwareMethodMatcher != null ?
     introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
     methodMatcher.matches(method, targetClass)) {
    return true;
   }
  }
 }
 return false;
}

小伙伴們看一下,這里就是先按照類去匹配,匹配通過則繼續(xù)按照方法去匹配,方法匹配器要是設(shè)置的 true,那就直接返回 true 就行了,否則就加載當(dāng)前類,也就是 targetClass,然后遍歷 targetClass 中的所有方法,最后調(diào)用 introductionAwareMethodMatcher.matches 方法去判斷方法是否和切點(diǎn)契合。

就這樣,我們就從所有的 Advisor 中找到了所有和當(dāng)前類匹配的 Advisor 了。

2.3.3 extendAdvisors

這個(gè)是添加一個(gè) DefaultPointcutAdvisor 切面進(jìn)來,這個(gè)切面使用的 Advice 是 ExposeInvocationInterceptor,ExposeInvocationInterceptor 的作用是用于暴露 MethodInvocation 對(duì)象到 ThreadLocal 中,如果其他地方需要使用當(dāng)前的 MethodInvocation 對(duì)象,直接通過調(diào)用 currentInvocation 方法取出即可。

這個(gè)方法的邏輯比較簡單,我就不貼出來了,小伙伴們可以自行查看。

2.4 createProxy

看完了 getAdvicesAndAdvisorsForBean 方法,我們已經(jīng)找到了適合我們的 Advisor,接下來繼續(xù)看 createProxy 方法,這個(gè)方法用來創(chuàng)建一個(gè)代理對(duì)象:

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
  @Nullable Object[] specificInterceptors, TargetSource targetSource) {
 return buildProxy(beanClass, beanName, specificInterceptors, targetSource, false);
}
private Object buildProxy(Class<?> beanClass, @Nullable String beanName,
  @Nullable Object[] specificInterceptors, TargetSource targetSource, boolean classOnly) {
 if (this.beanFactory instanceof ConfigurableListableBeanFactory clbf) {
  AutoProxyUtils.exposeTargetClass(clbf, beanName, beanClass);
 }
 ProxyFactory proxyFactory = new ProxyFactory();
 proxyFactory.copyFrom(this);
 if (proxyFactory.isProxyTargetClass()) {
  // Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios)
  if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) {
   // Must allow for introductions; can't just set interfaces to the proxy's interfaces only.
   for (Class<?> ifc : beanClass.getInterfaces()) {
    proxyFactory.addInterface(ifc);
   }
  }
 }
 else {
  // No proxyTargetClass flag enforced, let's apply our default checks...
  if (shouldProxyTargetClass(beanClass, beanName)) {
   proxyFactory.setProxyTargetClass(true);
  }
  else {
   evaluateProxyInterfaces(beanClass, proxyFactory);
  }
 }
 Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
 proxyFactory.addAdvisors(advisors);
 proxyFactory.setTargetSource(targetSource);
 customizeProxyFactory(proxyFactory);
 proxyFactory.setFrozen(this.freezeProxy);
 if (advisorsPreFiltered()) {
  proxyFactory.setPreFiltered(true);
 }
 // Use original ClassLoader if bean class not locally loaded in overriding class loader
 ClassLoader classLoader = getProxyClassLoader();
 if (classLoader instanceof SmartClassLoader smartClassLoader && classLoader != beanClass.getClassLoader()) {
  classLoader = smartClassLoader.getOriginalClassLoader();
 }
 return (classOnly ? proxyFactory.getProxyClass(classLoader) : proxyFactory.getProxy(classLoader));
}

這段代碼不知道小伙伴們看了是否覺得眼熟,這就是前面發(fā)的另類 AOP,編程式 AOP!一文中的內(nèi)容,所以這塊源碼大家自己看看就好了,我就不啰嗦了。


責(zé)任編輯:武曉燕 來源: 江南一點(diǎn)雨
相關(guān)推薦

2022-09-29 09:17:47

進(jìn)程Linux創(chuàng)建

2023-02-27 08:09:42

SpringAOP代理

2023-10-08 10:14:12

2022-09-01 10:40:29

SpringAOPJDK

2024-04-01 08:38:57

Spring@AspectAOP

2019-11-29 16:21:22

Spring框架集成

2022-06-24 09:36:47

Python對(duì)象調(diào)用

2012-07-11 14:31:16

SpringAop

2021-05-14 00:00:15

JavaScript開發(fā)代碼

2023-03-29 08:24:30

2025-01-16 08:45:48

2020-06-22 08:50:27

Spring AOP代理

2021-07-27 22:56:00

JavaScript編程開發(fā)

2021-07-01 10:45:18

Bean對(duì)象作用域

2024-11-04 16:29:19

2024-03-04 07:41:18

SpringAOPOOP?

2021-10-27 07:15:37

SpringAOP編程(

2010-04-26 08:53:06

面向方面編程.NET

2024-11-28 11:59:57

2009-06-16 15:02:18

面向?qū)ο缶幊?/a>PHP異常PHP代理
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)