在Spring AOP中代理對(duì)象創(chuàng)建的步驟詳解
1. AOP 用法
先來(lái)一個(gè)簡(jiǎn)單的案例,小伙伴們先回顧一下 AOP,假設(shè)我有如下類(lèi):
@Service public class UserService { public void hello() { System.out.println("hello javaboy"); } }
然后我寫(xiě)一個(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+" 方法開(kāi)始執(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ì)象。
2. 原理分析
那么注入到 Spring 容器中的 UserService,為什么在獲取的時(shí)候變成了一個(gè)代理對(duì)象,而不是原本的 UserService 了呢?
整體上來(lái)說(shuō),我們可以將 Spring Bean 的生命周期分為四個(gè)階段,分別是:
- 實(shí)例化。
- 屬性賦值。
- 初始化。
- 銷(xiāo)毀。
如下圖:
首先實(shí)例化就是通過(guò)反射,先把 Bean 的實(shí)例創(chuàng)建出來(lái);接下來(lái)屬性賦值就是給創(chuàng)建出來(lái)的 Bean 的各個(gè)屬性賦值;接下來(lái)的初始化就是給 Bean 應(yīng)用上各種需要的后置處理器;最后則是銷(xiāo)毀。
2.1 doCreateBean
AOP 代理對(duì)象的創(chuàng)建是在初始化這個(gè)過(guò)程中完成的,所以今天我們就從初始化這里開(kāi)始看起。
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è)方法,也都是非常常見(jiàn)的 Bean 初始化方法:
- invokeAwareMethods:執(zhí)行 Aware 接口下的 Bean。
- applyBeanPostProcessorsBeforeInitialization:執(zhí)行 BeanPostProcessor 中的前置方法。
- invokeInitMethods:執(zhí)行 Bean 的初始化方法 init。
- applyBeanPostProcessorsAfterInitialization:執(zhí)行 BeanPostProcessor 中的后置方法。
1、3 這兩個(gè)方法很明顯跟 AOP 關(guān)系不大,我們自己平時(shí)創(chuàng)建的 AOP 對(duì)象基本上都是在 applyBeanPostProcessorsAfterInitialization 中進(jìn)行處理的,我們來(lái)看下這個(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)類(lèi) 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ì)象,如果緩存中沒(méi)有的話(huà),則會(huì)調(diào)用 wrapIfNecessary 方法進(jìn)行 AOP 的創(chuàng)建。
正常來(lái)說(shuō),普通 AOP 的創(chuàng)建,前面三個(gè) if 的條件都是不滿(mǎn)足的。第一個(gè) if 是說(shuō) beanName 是否是一個(gè) targetSource,顯然我們這里不是;第二個(gè) if 是說(shuō)這個(gè) Bean 是不是不需代理,我們這里顯然是需要代理的。
關(guān)于第二個(gè) if 我多說(shuō)一句,如果這里進(jìn)來(lái)的是一個(gè)切面的 Bean,例如第一小節(jié)中的 LogAspect,這種 Bean 顯然是不需要代理的,所以會(huì)在第二個(gè)方法中直接返回,如果是其他普通的 Bean,則第二個(gè) if 并不會(huì)進(jìn)來(lái)。
所在在 wrapIfNecessary 中,最重要的方法實(shí)際上就是兩個(gè):getAdvicesAndAdvisorsForBean 和 createProxy,前者用來(lái)找出來(lái)所有跟當(dāng)前類(lèi)匹配的切面,后者則用來(lái)創(chuàng)建代理對(duì)象。
2.3 getAdvicesAndAdvisorsForBean
這個(gè)方法,說(shuō)白了,就是查找各種 Advice(通知/增強(qiáng)) 和 Advisor(切面)。來(lái)看下到底怎么找的:
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è)方法是查詢(xún)到所有候選的 Advisor,說(shuō)白了,就是把項(xiàng)目啟動(dòng)時(shí)注冊(cè)到 Spring 容器中所有切面都找到,由于一個(gè) Aspect 中可能存在多個(gè) Advice,每個(gè) Advice 最終都能封裝為一個(gè) Advisor,所以在具體查找過(guò)程中,找到 Aspect Bean 之后,還需要遍歷 Bean 中的方法。
- findAdvisorsThatCanApply:這個(gè)方法主要是從上個(gè)方法找到的所有切面中,根據(jù)切點(diǎn)過(guò)濾出來(lái)能夠應(yīng)用到當(dāng)前 Bean 的切面。
- extendAdvisors:這個(gè)是添加一個(gè) DefaultPointcutAdvisor 切面進(jìn)來(lái),這個(gè)切面使用的 Advice 是 ExposeInvocationInterceptor,ExposeInvocationInterceptor 的作用是用于暴露 MethodInvocation 對(duì)象到 ThreadLocal 中,如果其他地方需要使用當(dāng)前的 MethodInvocation 對(duì)象,直接通過(guò)調(diào)用 currentInvocation 方法取出即可。
接下來(lái)我們就來(lái)看一下這三個(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)鍵在于通過(guò) 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)來(lái)的時(shí)候,aspectNames 變量是沒(méi)有值的,所以會(huì)先進(jìn)入到 if 分支中,給 aspectNames 和 aspectBeanNames 兩個(gè)變量賦值。
具體過(guò)程就是首先調(diào)用 BeanFactoryUtils.beanNamesForTypeIncludingAncestors 方法,去當(dāng)前容器以及當(dāng)前容器的父容器中,查找到所有的 beanName,將返回的數(shù)組賦值給 beanNames 變量,然后對(duì) beanNames 進(jìn)行遍歷。
遍歷時(shí),首先調(diào)用 isEligibleBean 方法,這個(gè)方法是檢查給定名稱(chēng)的 Bean 是否符合自動(dòng)代理的條件的,這個(gè)細(xì)節(jié)我們就不看了,因?yàn)橐话闱闆r下,我們項(xiàng)目中的 AOP 都是自動(dòng)代理的。
接下來(lái)根據(jù) beanName,找到對(duì)應(yīng)的 bean 類(lèi)型 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ì)象。
接下來(lái)會(huì)去判斷 kind 是否為 SINGLETON,這個(gè)默認(rèn)都是 SINGLETON,所以這里會(huì)進(jìn)入到分支中,進(jìn)來(lái)之后,會(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
接下來(lái) findAdvisorsThatCanApply 方法主要是從眾多的 Advisor 中,找到能匹配上當(dāng)前 Bean 的 Advisor,小伙伴們知道,每一個(gè) Advisor 都包含一個(gè)切點(diǎn) Pointcut,不同的切點(diǎn)意味著不同的攔截規(guī)則,所以現(xiàn)在需要進(jìn)行匹配,檢查當(dāng)前類(lèi)需要和哪個(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 的類(lèi)型是否是 IntroductionAdvisor 類(lèi)型,IntroductionAdvisor 類(lèi)型的 Advisor 只能在類(lèi)級(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 類(lèi)型的 Advisor 只需要調(diào)用 ClassFilter 過(guò)濾一下就行了,小伙伴們看這里的匹配邏輯也是非常 easy!而 PointcutAdvisor 類(lèi)型的 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; }
小伙伴們看一下,這里就是先按照類(lèi)去匹配,匹配通過(guò)則繼續(xù)按照方法去匹配,方法匹配器要是設(shè)置的 true,那就直接返回 true 就行了,否則就加載當(dāng)前類(lèi),也就是 targetClass,然后遍歷 targetClass 中的所有方法,最后調(diào)用 introductionAwareMethodMatcher.matches
方法去判斷方法是否和切點(diǎn)契合。
就這樣,我們就從所有的 Advisor 中找到了所有和當(dāng)前類(lèi)匹配的 Advisor 了。
2.3.3 extendAdvisors
這個(gè)是添加一個(gè) DefaultPointcutAdvisor 切面進(jìn)來(lái),這個(gè)切面使用的 Advice 是 ExposeInvocationInterceptor,ExposeInvocationInterceptor 的作用是用于暴露 MethodInvocation 對(duì)象到 ThreadLocal 中,如果其他地方需要使用當(dāng)前的 MethodInvocation 對(duì)象,直接通過(guò)調(diào)用 currentInvocation 方法取出即可。
這個(gè)方法的邏輯比較簡(jiǎn)單,我就不貼出來(lái)了,小伙伴們可以自行查看。
2.4 createProxy
看完了 getAdvicesAndAdvisorsForBean 方法,我們已經(jīng)找到了適合我們的 Advisor,接下來(lái)繼續(xù)看 createProxy 方法,這個(gè)方法用來(lái)創(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)); }
好啦,經(jīng)過(guò)上面這一頓操作,代理對(duì)象就創(chuàng)建出來(lái)了~本文是一個(gè)大致的邏輯,還有一些特別細(xì)的小細(xì)節(jié)沒(méi)和小伙伴們梳理。
以上就是在Spring AOP中代理對(duì)象創(chuàng)建的步驟詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring AOP創(chuàng)建代理對(duì)象的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java mutable對(duì)象和immutable對(duì)象的區(qū)別說(shuō)明
這篇文章主要介紹了Java mutable對(duì)象和immutable對(duì)象的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06關(guān)于SpringCloud中Ribbon的7種負(fù)載均衡策略解析
這篇文章主要介紹了關(guān)于SpringCloud中Ribbon的7種負(fù)載均衡策略解析,服務(wù)端負(fù)載均衡器的問(wèn)題是,它提供了更強(qiáng)的流量控制權(quán),但無(wú)法滿(mǎn)足不同的消費(fèi)者希望使用不同負(fù)載均衡策略的需求,而使用不同負(fù)載均衡策略的場(chǎng)景確實(shí)是存在的,需要的朋友可以參考下2023-07-07Java?String源碼contains題解重復(fù)疊加字符串匹配
這篇文章主要為大家介紹了Java?String源碼contains題解重復(fù)疊加字符串匹配示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11java開(kāi)發(fā)微服務(wù)架構(gòu)設(shè)計(jì)消息隊(duì)列的水有多深
今天我們說(shuō)說(shuō)消息隊(duì)列的問(wèn)題,來(lái)帶大家探一探消息隊(duì)列的水有多深,希望看完本文大家在引入消息隊(duì)列的時(shí)候先想一想,是不是一定要引入?引入消息隊(duì)列后產(chǎn)生的問(wèn)題能不能解決2021-10-10java連接SQL?Server數(shù)據(jù)庫(kù)的超詳細(xì)教程
最近在java連接SQL數(shù)據(jù)庫(kù)時(shí)會(huì)出現(xiàn)一些問(wèn)題,所以這篇文章主要給大家介紹了關(guān)于java連接SQL?Server數(shù)據(jù)庫(kù)的超詳細(xì)教程,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06詳解Java刪除Map中元素java.util.ConcurrentModificationException”異常解決
這篇文章主要介紹了詳解Java刪除Map中元素java.util.ConcurrentModificationException”異常解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Netty搭建WebSocket服務(wù)器實(shí)戰(zhàn)教程
這篇文章主要介紹了Netty搭建WebSocket服務(wù)器實(shí)戰(zhàn),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-03-03SpringBoot整合RabbitMQ實(shí)現(xiàn)延遲隊(duì)列的示例詳解
這篇文章主要為大家詳細(xì)介紹了SpringBoot如何整合RabbitMQ實(shí)現(xiàn)延遲隊(duì)列,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的可以了解一下2023-04-04