詳解SpringMVC組件之HandlerMapping(二)
1、前言
在前面《詳解SpringMVC組件之HandlerMapping(一)》中,我們分析了HandlerMapping組件的整體邏輯及其AbstractUrlHandlerMapping系列的實(shí)現(xiàn)方式。
這一節(jié),我們將分析AbstractHandlerMethodMapping系列的實(shí)現(xiàn)方式。
2、AbstractHandlerMethodMapping體系
在AbstractHandlerMethodMapping體系中,只有三個類,分別是
- AbstractHandlerMethodMapping
- RequestMappingInfoHandlerMapping
- RequestMappingHandlerMapping
這三個類依次繼承于前面的類,而最前面的AbstractHandlerMethodMapping抽象類則繼承于AbstractHandlerMapping抽象類,并實(shí)現(xiàn)了InitializingBean接口,實(shí)現(xiàn)了InitializingBean接口后,就會在Bean實(shí)例化后調(diào)用afterPropertiesSet()方法。
在AbstractHandlerMethodMapping體系中,類的層級結(jié)構(gòu)比較簡單明確,但是Spring為了保證AbstractHandlerMethodMapping體系的靈活性,邏輯還是比較復(fù)雜的。
在分析AbstractHandlerMethodMapping類之前,我們先認(rèn)識一下其中的幾個核心的基礎(chǔ)類。
- HandlerMethod
- 一個基于方法的處理器,包括了該處理器對應(yīng)的方法和實(shí)例Bean,并提供了一些訪問方法參數(shù)、方法返回值、方法注解等方法。
- RequestMappingInfo
- 表示請求信息,并封裝了映射關(guān)系的匹配條件。使用@RequestMapping注解時,配置的信息最后都設(shè)置到了RequestMappingInfo中,@RequestMapping注解的不同屬性,會映射到對應(yīng)的XXXRequestCondition上。
- AbstractHandlerMethodMapping-內(nèi)部類Match
- 封裝了HandlerMethod和泛型類T。泛型類T實(shí)際上表示了RequestMappingInfo。
- 內(nèi)部類MappingRegistration
- 記錄映射關(guān)系注冊時的信息。封裝了HandlerMethod、泛型類T、mappingName、directUrls屬性(保存url和RequestMappingInfo對應(yīng)關(guān)系,多個url可能對應(yīng)著同一個mappingInfo)
- 內(nèi)部類MappingRegistry
- 主要維護(hù)幾個Map,用來存儲映射的信息。下面詳細(xì)介紹該內(nèi)部類及其定義的映射關(guān)系
3、AbstractHandlerMethodMapping抽象類
在AbstractHandlerMethodMapping抽象類中定義了很多個內(nèi)部類,前面提到的Match、MappingRegistration、MappingRegistry均是在該類中,其中,又以MappingRegistry最重要,下面我們首先分析這個內(nèi)部類,因?yàn)槠渲猩婕暗降膸讉€Map屬性,是維護(hù)request與處理器Handler間關(guān)系的核心所在。
3.1、MappingRegistry內(nèi)部類
MappingRegistry內(nèi)部類主要維護(hù)了T(RequestMappingInfo)、mappingName、HandlerMethod、CorsConfiguration等對象間的映射關(guān)系,同時提供了一個讀寫鎖readWriteLock對象。
1、屬性
//維護(hù)mapping與MappingRegistration注冊信息的關(guān)系 private final Map<T, MappingRegistration<T>> registry = new HashMap<>(); //維護(hù)mapping與HandlerMethod的關(guān)系 private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>(); //保存著URL與匹配條件(mapping)的對應(yīng)關(guān)系,key是那些不含通配符的URL,value對應(yīng)的是一個list類型的值。 private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>(); //保存著name和HandlerMethod的對應(yīng)關(guān)系(一個name可以有多個HandlerMethod) private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>(); //保存HandlerMethod與跨域配置的關(guān)系 private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>(); //讀寫鎖 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
2、register()方法、unregister()方法 在register()方法、unregister()方法中主要是用來注冊T(RequestMappingInfo)、mappingName、HandlerMethod、CorsConfiguration等對象間的映射關(guān)系。
其中,register()方法代碼如下:
public void register(T mapping, Object handler, Method method) {
// Assert that the handler method is not a suspending one.
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
throw new IllegalStateException("Unsupported suspending handler method detected: " + method);
}
//添加寫鎖
this.readWriteLock.writeLock().lock();
try {
//創(chuàng)建一個HandlerMethod對象(構(gòu)造函數(shù)創(chuàng)建對象)
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
//驗(yàn)證handlerMethod和mapping的對應(yīng)關(guān)系,如果已經(jīng)存在一個HandlerMethod對象且與handlerMethod不一樣,就會拋出異?!癆mbiguous mapping. Cannot map”
validateMethodMapping(handlerMethod, mapping);
//維護(hù)mappingLookup對象,建立mapping和handlerMethod的映射關(guān)系
this.mappingLookup.put(mapping, handlerMethod);
//獲取匹配條件mapping,對應(yīng)的URL(那些不含通配符的URL),且URL是由子類實(shí)現(xiàn)getMappingPathPatterns()方法提供的,實(shí)際上還是由PatternsRequestCondition提供的,那個該directUrls 是什么時候初始化的呢?我們后續(xù)在分析。
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
String name = null;
//獲取mappingName,并維護(hù)與handlerMethod的關(guān)系
if (getNamingStrategy() != null) {
//namingStrategy屬性對應(yīng)的是RequestMappingInfoHandlerMethodMappingNamingStrategy對象(是在子類RequestMappingInfoHandlerMapping構(gòu)造函數(shù)中進(jìn)行了設(shè)置),命名規(guī)則:類名中全部大小字符+“#”+方法名
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}
//獲取跨域配置CorsConfiguration對象,維護(hù)與handlerMethod的關(guān)系。initCorsConfiguration()定了空方法,在RequestMappingHandlerMapping類中進(jìn)行了重寫。
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}
//維護(hù)mapping與MappingRegistration的關(guān)系
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}
finally {
this.readWriteLock.writeLock().unlock();
}
}而unregister()方法主要是移除mapping匹配條件與其他對象的映射關(guān)系,這里不在貼出代碼了。
3.2、初始化
在前面我們提到了AbstractHandlerMethodMapping實(shí)現(xiàn)了InitializingBean接口,所以就會在Bean實(shí)例化后調(diào)用afterPropertiesSet()方法。
其實(shí),AbstractHandlerMethodMapping類的初始化工作也就是從這個地方開始的。
代碼如下:
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
protected void initHandlerMethods() {
//獲取候選的bean實(shí)例
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
//遍歷后續(xù)的bean實(shí)例,并分別進(jìn)行處理
processCandidateBean(beanName);
}
}
//只是打印了日志,沒有實(shí)際的處理邏輯
handlerMethodsInitialized(getHandlerMethods());
}在afterPropertiesSet()方法中,調(diào)用了initHandlerMethods()方法,實(shí)際初始化邏輯就是在該方法中實(shí)現(xiàn)的。首先通過getCandidateBeanNames()方法獲取所有候選的Bean實(shí)例的name,代碼如下:
protected String[] getCandidateBeanNames() {
//detectHandlerMethodsInAncestorContexts 表示是否從祖先容器中查找bean實(shí)例
return (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
obtainApplicationContext().getBeanNamesForType(Object.class));
}然后,遍歷通過getCandidateBeanNames()方法獲取的Bean實(shí)例,調(diào)用processCandidateBean()方法進(jìn)行處理,代碼如下:
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
//獲取對應(yīng)的Class類型
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
//判斷beanType是不是處理器類型,通過isHandler()方法判斷,該方法在RequestMappingHandlerMapping類中實(shí)現(xiàn),根據(jù)是否有Controller或RequestMapping注解進(jìn)行判斷
if (beanType != null && isHandler(beanType)) {
//獲取指定Bean實(shí)例中對應(yīng)的處理方法,并通過mappingRegistry注冊到對應(yīng)的Map關(guān)系映射中
detectHandlerMethods(beanName);
}
}在processCandidateBean()方法中,判斷beanName對應(yīng)的Bean實(shí)例是否是處理器類型(isHandler方法判斷),如果是的話,則調(diào)用detectHandlerMethods()方法,獲取該實(shí)例中對應(yīng)的處理方法,并通過mappingRegistry注冊到對應(yīng)的Map關(guān)系映射中,代碼如下:
protected void detectHandlerMethods(Object handler) {
//獲取處理器對應(yīng)的Class
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
//獲取用戶定義的本來的類型,大部分情況下就是類型本身,主要針對cglib做了額外的判斷,獲取cglib代理的父類;
Class<?> userType = ClassUtils.getUserClass(handlerType);
//查找給定類型userType中的指定方法,具體判斷條件由getMappingForMethod()方法來決定
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
//把篩選出來的方法,通過registerHandlerMethod()注冊到映射關(guān)系中
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}自此,初始化工作就完成了,即注冊了mapping匹配條件與HandlerMethod的映射關(guān)系。
3.3、getHandlerInternal()方法
在前面分析了AbstractHandlerMethodMapping類的初始化方法,現(xiàn)在我們開始分析該類如何實(shí)現(xiàn)父類的getHandlerInternal()方法,即如何通過request獲取對應(yīng)的處理器HandlerMethod對象。
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
//獲取lookupPath
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
//設(shè)置“org.springframework.web.servlet。HandlerMapping.lookupPath”參數(shù)
request.setAttribute(LOOKUP_PATH, lookupPath);
//獲取讀鎖
this.mappingRegistry.acquireReadLock();
try {
//獲取request對應(yīng)的處理器
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
//如果處理器對應(yīng)的是bean name,則調(diào)用createWithResolvedBean()方法,創(chuàng)建對應(yīng)的Bean實(shí)例
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}在getHandlerInternal()方法中,我們知道lookupHandlerMethod()方法是真正實(shí)現(xiàn)查找處理器的方法,代碼如下:
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
//從MappingRegistry.urlLookup屬性中,獲取lookupPath對應(yīng)的mapping集合
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
//獲取匹配的mapping,添加到matches變種
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// 如果沒有匹配lookupPath的實(shí)例,則遍歷所有的mapping,查找符合條件的mapping
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {//說明存在符合條件的mapping,可能是多個
//獲取匹配條件的排序器,由抽象方法getMappingComparator()方法獲取,該方法由子類實(shí)現(xiàn)
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
//排序
matches.sort(comparator);
Match bestMatch = matches.get(0);
if (matches.size() > 1) {//不止一個匹配時
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(request)) {//OPTIONS請求時,直接處理
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
//如果存在多個處理器,則直接拋出異常
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
//定義“.bestMatchingHandler”屬性
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
//處理匹配的處理器,這里只是添加了一個".pathWithinHandlerMapping"屬性,具體實(shí)現(xiàn)在子類中進(jìn)行。
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {//不存在匹配的bean時,調(diào)用handleNoMatch()方法,空方法,有子類進(jìn)行實(shí)現(xiàn)。
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}4、RequestMappingInfoHandlerMapping抽象類
RequestMappingInfoHandlerMapping抽象類繼承自AbstractHandlerMethodMapping類,并明確了其中的泛型類為RequestMappingInfo。
在RequestMappingInfoHandlerMapping抽象類中做了以下幾件事:
1、定義了Options的默認(rèn)處置方法,其中涉及到了HTTP_OPTIONS_HANDLE_METHOD常量、內(nèi)部類HttpOptionsHandler和靜態(tài)代碼塊初始化常量。
2、構(gòu)造函數(shù),在構(gòu)造函數(shù)中設(shè)置了映射的命名策略,即RequestMappingInfoHandlerMethodMappingNamingStrategy實(shí)現(xiàn)的方式。
3、實(shí)現(xiàn)了父類中的幾個方法,如下所示:
//獲取RequestMappingInfo 對應(yīng)的URL集合
@Override
protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
return info.getPatternsCondition().getPatterns();
}
//判斷當(dāng)前的RequestMappingInfo與request是否匹配,實(shí)際上是由RequestMappingInfo的getMatchingCondition()方法實(shí)現(xiàn)判斷,并返回一個新建的RequestMappingInfo 實(shí)例
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
return info.getMatchingCondition(request);
}
//獲取比較器
@Override
protected Comparator<RequestMappingInfo> getMappingComparator(final HttpServletRequest request) {
return (info1, info2) -> info1.compareTo(info2, request);
}4、重新父類的handleMatch()方法,在該方法中主要添加了處理變量(模板變量和矩陣變量(MatrixVariable))和媒體類型的邏輯。后續(xù)再詳細(xì)分析參數(shù)處理的過程。
5、重寫父類的handleNoMatch()方法,該方法中使用了內(nèi)部類PartialMatchHelper來判斷不匹配的原因,然后拋出指定的異常。不在貼出代碼。
5、RequestMappingHandlerMapping類
RequestMappingHandlerMapping類除了繼承了RequestMappingInfoHandlerMapping之外,還實(shí)現(xiàn)了MatchableHandlerMapping和EmbeddedValueResolverAware兩個接口。
其中,實(shí)現(xiàn)MatchableHandlerMapping接口的方法,暫時未使用;而實(shí)現(xiàn)了EmbeddedValueResolverAware接口,說明要支持解析String字符串。
定義的屬性:
//是否啟用后綴匹配 private boolean useSuffixPatternMatch = true; //后綴模式匹配是否應(yīng)該只對顯式地在ContentNegotiationManager中注冊的路徑擴(kuò)展有效。 private boolean useRegisteredSuffixPatternMatch = false; //尾部斜杠匹配 private boolean useTrailingSlashMatch = true; //根據(jù)條件設(shè)置前綴path private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>(); //多媒體類型判斷 private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); //字符串解析器,處理spring表達(dá)式 @Nullable private StringValueResolver embeddedValueResolver; //RequestMappingInfo構(gòu)建配置 private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
afterPropertiesSet()方法
在前面,我們知道afterPropertiesSet()方法是實(shí)現(xiàn)初始化的方法。在AbstractHandlerMethodMapping抽象類中,實(shí)現(xiàn)了handler類和方法的檢測和注冊。在RequestMappingHandlerMapping類中,又增加了RequestMappingInfo構(gòu)建配置的初始化,代碼如下:
@Override
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
this.config.setContentNegotiationManager(getContentNegotiationManager());
super.afterPropertiesSet();
}isHandler()方法
在AbstractHandlerMethodMapping類中,進(jìn)行初始化的時候,在processCandidateBean()方法中使用了isHandler判斷當(dāng)前bean實(shí)例是否是處理器。實(shí)際判斷邏輯在這里實(shí)現(xiàn)的。
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}getMappingForMethod方法
在AbstractHandlerMethodMapping類中,進(jìn)行初始化的時候,在detectHandlerMethods()方法中,調(diào)用該方法實(shí)現(xiàn)處理器方法的判斷。
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).build().combine(info);
}
}
return info;
}
@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class ?
getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
protected RequestMappingInfo createRequestMappingInfo(
RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name());
if (customCondition != null) {
builder.customCondition(customCondition);
}
return builder.options(this.config).build();
}其他方法 在registerMapping()、registerHandlerMethod()方法這兩個方法,除了調(diào)用父類的方法之外, 主要是設(shè)置了ConsumesRequestCondition的判斷條件。
后續(xù)還有配置跨域相關(guān)參數(shù),這里不在詳細(xì)分析了。
6、總結(jié)
在這篇文章中,我們只是分析了AbstractHandlerMethodMapping體系實(shí)現(xiàn)的HandlerMapping中這三個類的基本實(shí)現(xiàn),其中涉及到的RequestCondition及RequestMappingInfo(也是RequestCondition的子類)還有HandlerMethod等類的具體介紹,我們在后續(xù)過程中在逐漸的學(xué)習(xí)記錄。
到此這篇關(guān)于詳解SpringMVC組件之HandlerMapping(二)的文章就介紹到這了,更多相關(guān)SpringMVC的HandlerMapping內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java面向?qū)ο笤O(shè)計原則之單一職責(zé)與依賴倒置原則詳解
這篇文章主要介紹了java面向?qū)ο笤O(shè)計原則之單一職責(zé)與依賴倒置原則的分析詳解,有需要的朋友可以借鑒參考下,希望可以有所幫助,祝大家多多進(jìn)步早日升職加薪2021-10-10
java application maven項(xiàng)目打自定義zip包實(shí)例(推薦)
下面小編就為大家?guī)硪黄猨ava application maven項(xiàng)目打自定義zip包實(shí)例(推薦)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05
SSH框架網(wǎng)上商城項(xiàng)目第17戰(zhàn)之購物車基本功能
這篇文章主要為大家詳細(xì)介紹了SSH框架網(wǎng)上商城項(xiàng)目第17戰(zhàn)之購物車基本功能的實(shí)現(xiàn)過程,感興趣的小伙伴們可以參考一下2016-06-06
詳解Spring Boot工程集成全局唯一ID生成器 UidGenerator的操作步驟
本文就在項(xiàng)目中來集成 UidGenerator這一工程來作為項(xiàng)目的全局唯一 ID生成器。接下來通過實(shí)例代碼給大家詳解詳解Spring Boot工程集成全局唯一ID生成器 UidGenerator的操作步驟,感興趣的朋友一起看看吧2018-10-10
Java實(shí)現(xiàn)基于UDP協(xié)議的網(wǎng)絡(luò)通信UDP編程
在Java中使用UDP編程,仍然需要使用Socket,因?yàn)閼?yīng)用程序在使用UDP時必須指定網(wǎng)絡(luò)接口(IP地址)和端口號。注意:UDP端口和TCP端口雖然都使用0~65535,但他們是兩套獨(dú)立的端口,即一個應(yīng)用程序用TCP占用了端口1234,不影響另一個應(yīng)用程序用UDP占用端口12342023-04-04
Java數(shù)據(jù)結(jié)構(gòu)之紅黑樹的實(shí)現(xiàn)方法和原理詳解
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)之紅黑樹的實(shí)現(xiàn)方法和原理,紅黑樹是一種特殊的二叉查找樹,每個結(jié)點(diǎn)都要儲存位表示結(jié)點(diǎn)的顏色,或紅或黑,本文將通過示例為大家詳細(xì)講講紅黑樹的原理及實(shí)現(xiàn),感興趣的朋友可以了解一下2024-02-02
Spring Boot應(yīng)用事件監(jiān)聽示例詳解
這篇文章主要給大家介紹了關(guān)于Spring Boot應(yīng)用事件監(jiān)聽的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-12-12

