Spring的请求映射handlerMapping以及原理详解
请求映射原理
也就是说我们每次发请求,它到底是怎么找到我们哪个方法来去处理这个请求,因为我们知道所有的请求过来都会来到 DispatcherServlet 。
springboot 底层还是使用的是 springMVC 所以 springMVC 的 DispatcherServlet 是处理所以请求的开始,他的整个请求处理方法是,我们来找一下:
DispatcherServlet 说起来也是一个 servlet 它继承 FrameworkServlet 又继承于 HttpServletBean 又继承于 HttpServlet 。
说明
DispatcherServlet 是一个 HttpServlet ,继承于 Servlet 必须重写 doGet 或 doPost 之类的方法 Ctril + F12 (打开 HttpServlet 整个结构)我们发现这里没有 doGet() 或 doSet() 方法,那说明子类里面有没有重写。
它的继承树是:
DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
我们原生的 Servlet 本来有 doGet 和 doPost 方法,但是我们发现在 HttpServletBean 里面没有找到,那就在 FrameworkServlet 里面有没有, Ctril + F12 看看有没有 doGet 和 doPost 。
protected final void doGet() 是继承了 HttpServletBean 。
在 FrameworkServlet 里 我们发现无论是 doGet 还是 doPost 最终都是调用我们本类的 processRequest 说明我们请求处理一开始我们 HttpServlet 的 doGet 最终会调用到我们 FrameworkServlet 里面的 processRequest
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long startTime = System.currentTimeMillis(); Throwable failureCause = null; LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); LocaleContext localeContext = this.buildLocaleContext(request); RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor()); this.initContextHolders(request, localeContext, requestAttributes); //这些都是初始化过程 try { this.doService(request, response); } catch (IOException | ServletException var16) { failureCause = var16; throw var16; } catch (Throwable var17) { failureCause = var17; throw new NestedServletException("Request processing failed", var17); } finally { this.resetContextHolders(request, previousLocaleContext, previousAttributes); if (requestAttributes != null) { requestAttributes.requestCompleted(); } this.logResult(request, response, (Throwable)failureCause, asyncManager); this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause); } }
processRequest -> doService()
我们尝试执行一个 doService() 方法,执行完后都是一些清理过程( catch )
protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;
它由于是一个抽象方法( abstract ),他也没有重写和实现,只能来到子类( DispatcherServlet )来到 DispatcherServlet 来找 doService() :
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { this.logRequest(request); Map<String, Object> attributesSnapshot = null; if (WebUtils.isIncludeRequest(request)) { attributesSnapshot = new HashMap(); Enumeration attrNames = request.getAttributeNames(); label116: while(true) { String attrName; do { if (!attrNames.hasMoreElements()) { break label116; } attrName = (String)attrNames.nextElement(); } while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet")); attributesSnapshot.put(attrName, request.getAttribute(attrName)); } } request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext()); request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource()); if (this.flashMapManager != null) { FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); if (inputFlashMap != null) { request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap)); } request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); } RequestPath previousRequestPath = null; if (this.parseRequestPath) { previousRequestPath = (RequestPath)request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); ServletRequestPathUtils.parseAndCache(request); } try { this.doDispatch(request, response); } finally { if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) { this.restoreAttributesAfterInclude(request, attributesSnapshot); } if (this.parseRequestPath) { ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); } } }
也就是说最终 DispatcherServlet 里面对 doService() 进行了实现
只要看到 get 、 set 都是在里面放东西进行初始化的过程
我们一连串 请求一进来应该是调HttpServlet的doGet,在FrameworkServlet重写了 唯一有效语句是抽象类doService()方法,而这个方法在DispatcherServlet类中实现
核心的方法调用
try { this.doDispatch(request, response); } finally { if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) { this.restoreAttributesAfterInclude(request, attributesSnapshot); } if (this.parseRequestPath) { ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); } }
叫 doDispatch
finally { if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) { this.restoreAttributesAfterInclude(request, attributesSnapshot); } if (this.parseRequestPath) { ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); } }
意思是把我们 doDispatch() 请求做派发,看看 doDispatch() :
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { try { ModelAndView mv = null;//初始化数据 Object dispatchException = null;//初始化数据 try { processedRequest = this.checkMultipart(request);//检查我们是否有文件上传请求 multipartRequestParsed = processedRequest != request;//如果是文件上传请求它在这进行一个转化 // Determine handler for the current request. 就是我们来决定哪个handler(Controller)能处理当前请求(current) mappedHandler = this.getHandler(processedRequest); if (mappedHandler == null) { this.noHandlerFound(processedRequest, response); return; } HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } this.applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception var20) { dispatchException = var20; } catch (Throwable var21) { dispatchException = new NestedServletException("Handler dispatch failed", var21); } this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); } catch (Exception var22) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22); } catch (Throwable var23) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23)); } } finally { if (asyncManager.isConcurrentHandlingStarted()) { if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else if (multipartRequestParsed) { this.cleanupMultipart(processedRequest); } } }
我们发现这里才是真正有功能的方法
processedRequest = this.checkMultipart(request); //checkMultipart():检查文件上传
doDispatch() 才是我们 DispatcherServlet 里面最终要研究的方法,每一个请求进来都要调用 doDispatch 方法
断点
我们打上断点,来看整个请求处理, 包括 它是怎么找到我们每一个请求要调用谁来处理的
如果我来发送请求(登录( localhost:8080 ))放行,知道页面出来后我们点击 REST-GET 请求,我们来看, protected void doDispatch(HttpServletRequest request, HttpServletResponse response) 传入原生的 request 和 response ;我们点进 request 里面我们发现我们整个请求的路径( coyoteRequest )是 /user ,请求的详细信息都在这( coyoteRequest ),路径( decodeUriMB )是 /user 。我们接下开看要用谁调用的。
HttpServletRequest processedRequest = request; 相当于把原生的请求( request )拿过来包装一下( processedRequest )。
这有个 HandlerExecutionChain mappedHandler = null; 执行量我们后来再说
然后继续,说 multipartRequestParsed 是不是一个文件上传请求,默认是 false ,然后包括我们整个请求期间有没有异步( getAsyncManager() ),如果有异步使用异步管理器( WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); )暂时在这块我们不用管。
注意 :只有一个 ModelAndView mv = null;Object dispatchException = null; 这些都是空初始化的一些数据。加下来看我们第一个有功能的一块:在 doDispatch() 里面第一个有功能的叫 checkMultipart (检查我们是否有文件上传请求,响应文件上传再说)如果是文件上传请求它在这进行一个转化( multipartRequestParsed = processedRequest != request; )注意这有一个 // Determine handler for the current request. , 就是我们来决定哪个handler能处理当前请求(current) 我们放行( mappedHandler = this.getHandler(processedRequest); ) mappedHandler 就会看到:它直接给我们找到了 HelloCOntroller 的 getUser() 方法来处理这个请求
神奇的地方就在这个(mappedHandler = this.getHandler(processedRequest);) 它到底是怎么找到我当前的 /User 请求会要调用那个方法进行处理的。
我们从 HttpServletRequest processedRequest = request; 直接放行到 mappedHandler = this.getHandler(processedRequest); , getHandler 他要依据当前请求( processedRequest )当前请求里面肯定有哪个 url 地址这是 http 传过来的不用管( Step into )进来,这里有一个东西叫 handlerMappings 也就是获取到所有的(这个 handlerMappings 有五个)
1、handlerMapping:处理器映射
也就是说我们springMVC怎么知道哪个请求要用谁处理,是根据处理器里面的映射规则。
也就是说:
/xxx请求 -> xxx处理 都有这映射规则,而这些规则都被保存到 handlerMapping 里面,如上图所示,我们现在有5个 handlerMapping ,其中有几个 handlerMapping 大家可能有点熟悉,比如: WelcomePageHandlerMapping (欢迎页的处理请求),我们之前说资源管理规则的时候我们发现我们springMVC自动的会给容器中放一个欢迎页的 handlerMapping 然后这个 handlerMapping ()里面也有保存规则,保存什么规则?就是我们所有的 index 请求你的比如这个 PathMatcher (路径匹配)我们这个 / (当前项目下的’/‘你直接访问这个)我给你访问到哪?我们的’/‘会直接 rootHandler下的View路径 (这里是等于到了’index’)所以我们首页要访问到的是我们这个 WelcomePageHandlerMapping 里面保存了一个规则,所以就有首页的访问了。
我们加下来还有一个 RequestMappingHandlerMapping
2、RequestMappingHandlerMapping
我们以前有一个注解叫 @RequestMapping 相当于是 @RequestMapping 注解的所有处理器映射,也就是说这个东西( RequestMappingHandlerMapping )里面保存了所有 @RequestMapping 和 handler 的规则。而它又是怎么保存的,那其实是 我们的应用一启动springMVC自动扫描我们所有的 Controller 并解析注解,把你的这些注解信息全部保存到HandlerMapping里面 。所以它会在这五个 handlerMapping 里面(大家注意增强for循环)挨个找我们所有的请求映射,看谁能处理这个请求,我们找到第一个 handlerMapping 相当于我们的 RequestMappingHandlerMapping ,它里面保存了哪些映射信息,有个 mappingRegistry (相当于我们映射的注册中心)这个中心里面打开你就会发现
我们当前项目里写的所有的路径它在这个都有映射: POST[/user] 是哪个Controller哪个方法处理的,包括系统自带的 /error 它是哪个 Controller 哪个方法处理的
相当于是我们 RequestMappingHandlerMapping 保存了我们当前系统我们每一个自己写的类,每一个类每一个方法都能处理什么请求。我们当前请求 /user ,我们遍历到第一个 HandlerMapping 的时候相当于它的注册中心( mappingRegistry )里面就能找到 /user 是谁来处理,所以最终在这决定是在 HelloController#getUser 来处理的。所以它在这( HandlerExecutionChain handler = mapping.getHandler(request); )所以它在 mapping 里面 getHandler ( mapping.getHandler );从我们的 HandlerMapping 里面获取( getHandler() )我们的 handler 就是处理器,点进 getHandler()
@Override @Nullable public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { Object handler = getHandlerInternal(request); if (handler == null) { handler = getDefaultHandler(); } if (handler == null) { return null; } // Bean name or resolved handler? if (handler instanceof String) { String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } // Ensure presence of cached lookupPath for interceptors and others if (!ServletRequestPathUtils.hasCachedPath(request)) { initLookupPath(request); } HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); if (logger.isTraceEnabled()) { logger.trace("Mapped to " + handler); } else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) { logger.debug("Mapped to " + executionChain.getHandler()); } if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) { CorsConfiguration config = getCorsConfiguration(handler, request); if (getCorsConfigurationSource() != null) { CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request); config = (globalConfig != null ? globalConfig.combine(config) : config); } if (config != null) { config.validateAllowCredentials(); } executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; }
getHandlerInterna() 我们来获取,怎么获取( step into ):
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); try { return super.getHandlerInternal(request); } finally { ProducesRequestCondition.clearMediaTypesAttribute(request); } }
( step into ) getHandlerInternal() :
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { String lookupPath = initLookupPath(request); this.mappingRegistry.acquireReadLock();//拿到一把锁 try { HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } finally { this.mappingRegistry.releaseReadLock(); } }
它先拿到 request 原生请求,我们现在想要访问的路径( lookupPath )我们想要访问的路径是 /user 然后带着这个路径,它还拿到一把锁( acquireReadLock() )害怕我们并发查询我们这个 mappingRegistry 这个 mappingRegistry 也看到了是我们这个 RequestMappingHandlerMapping , handlerMapping 里面的一个属性 mappingRegistry 它里面保存了我们所有请求调用哪个方法处理
所以它最终相当于是在我们当前请求( request )这个路径( lookupPath )到底谁来处理( handlerMethod )
(step into):
@Nullable protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList<>(); List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, request); } if (matches.isEmpty()) {//没找到 addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);//添加一些空的东西 } if (!matches.isEmpty()) { Match bestMatch = matches.get(0);//它把它找到的你里面的第一个拿过来 if (matches.size() > 1) { Comparator<Match> comparator = new MatchComparator(getMappingComparator(request)); matches.sort(comparator); bestMatch = matches.get(0); if (logger.isTraceEnabled()) { logger.trace(matches.size() + " matching mappings: " + matches); } if (CorsUtils.isPreFlightRequest(request)) { for (Match match : matches) { if (match.hasCorsConfig()) { return PREFLIGHT_AMBIGUOUS_MATCH; } } } else { Match secondBestMatch = matches.get(1); if (comparator.compare(bestMatch, secondBestMatch) == 0) { Method m1 = bestMatch.getHandlerMethod().getMethod(); Method m2 = secondBestMatch.getHandlerMethod().getMethod(); String uri = request.getRequestURI(); throw new IllegalStateException( "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}"); } } } request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod()); handleMatch(bestMatch.mapping, lookupPath, request); return bestMatch.getHandlerMethod(); } else { return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request); } }
形参:
lookupPath(当前我们要找的路径);request(我们原生的请求),接下来就在下面开始找。
从我们这个 Registry ( mappingRegistry )里面,使用我们这个路径( getMappingsByDirectPath(lookupPath) )然后去找谁能处理;
问题就是在于我们这个 Registry ( RequestMappingHandlerMapping )里面他的路径光靠 /user 请求其实有四个人的路径都是这样只是请求方式不对。
getMappingsByDirectPath()
先是根据 url ( getMappingsByDirectPath() 视频里是 getMappingByUrl() )来找,按照前面的来能找到4个( directPathMatches (+ArraysList@7307 size=4) )( GET 、 POST 、 PUT 、 DELETE 方式的user)找到了以后接下来它把所有找到的添加到我们这个匹配的集合里面( addMatchingMappings(directPathMatches, matches, request); )
如果没找到( if (matches.isEmpty()) )它就添加一些空的东西( addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request); ),如果找到了还不为空( if (!matches.isEmpty()) )接下来它把它找到的你里面的第一个拿过来( Match bestMatch = matches.get(0); )
如果它同时找到了很多他就认为第一个是最匹配的,而且大家注意: 如果我们现在的 matches.size() 大于1( if (matches.size() > 1) )也就是相当于我们找到了非常多的 matches ;
注意:这个 matches 在这( addMatchingMappings(directPathMatches, matches, request); ),在这一块( directPathMatches )找到了四个,它( matches )把这四个调用 addMatchingMappings() 这个方法获取到能匹配的集合里面( matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping))); ); addMatchingMappings() 肯定以请求方式匹配好了
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) { for (T mapping : mappings) { T match = getMatchingMapping(mapping, request); if (match != null) { matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping))); } } }
所以最终给我们留下我们最佳匹配的这个 matches 里面集合里面只有一个,他( bestMatch = matches.get(0); )就拿到这个,所以最终给我们留下一个我们最佳匹配的我们这个 matches (里面只有一个),然后它( matches.get(0); )就拿到这个( matches ),这个 matches ( matches.get(0); )已经得到最佳匹配的了,如果你写了多个方法同时都能处理 /GET 请求,那你的这个 matches 就能大于1( if (matches.size() > 1) )大于1后(if里面)各种排序排完以后在这( Match secondBestMatch = matches.get(1); )最后给你测试,把你能匹配的一俩个全都拿来进行对比
最终比完后给你抱一个错说: throw new IllegalStateException( "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}"); (相当于我们这个 handler 你能处理这个 uri (路径) 有俩个方法都能处理)说明就会抛出异常( IllegalStateException ),所以 springMVC 要求我们:同样的一个请求方式不能有多个方法同时能处理,只能有一个。
springMVC 要求我们:同样的一个请求方式不能有多个方法同时能处理,只能有一个(原因在上面一段)
最终我们就找到我们最佳匹配规则(GET方式的user)能匹配,而GET方式的User是 Controller#getUser() 方法
所以简单总结起来就是一句话:怎么知道哪个请求谁能处理?所有的请求映射都保存在了``HandlerMapping`中。
我们 springboot 一启动给我们配置了 welcomePageHandlerMapping (欢迎页的 handlerMapping )
- springboot自动配置欢迎页的handlerMaping。访问’/'能访问到’index’页面
- 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- 如果有就找个请求对象的handler
- 如果没有就是下一个HandlerMapping
总结
(这里以欢迎页为例) localhost:8080/ 进入到首页,我们请求一进来( HttpServletRequest processedRequest = request; )它来找( mappedHandler = getHandler(processedRequest); (当前请求访问的是 / 的请求))看谁能处理( getHandler() )接下来就进入了遍历循环( forEach )所有 HandlerMapping 的时候了,我们先来找到第一个 handlerMapping ( HandlerExecutionChain handler = mapping.getHandler(request); )第一个 HandlerMapping 由于我们这个里面映射只保存了相当于是我们自己 Controller 写的这个路径映射没人能处理,所以如果在第一个里面找,你找到的handler肯定是空的,找到是空的,所以继续for循环;再次for循环来到第二个 handlerMapping 叫 WelcomePageHandlerMapping ,它正好处理的路径就是’/'所以我们现在找,就找到 handler 了,而这个 handler 是什么?就是我们 springMVC 里面默认处理我们’index’页面,人家给我们的 ViewController 访问我们的’index’页面就有人了。这就是 handlerMapping 的匹配规则。所有的 handleMapping 全部来进行匹配谁能匹配用谁的 springboot帮我们配置了哪些HandlerMapping
我们来到webMvcAutoCOnfig看看有没有跟HandlerMapping有关的:
首先第一个:
- RequestMappingHandlerMapping
- 相当于我们容器中(@Bean),我们第一个HandlerMaping是我们springboot给我们容器放的默认的。 这个组件就是来解析我们当前所有的方法(Controller里的方法)标了@RequestMapping注解(GET PUT都一样)标了这些注解的时候它(RequestMappingHandlerMapping)整的。
- WelcomePageHandlerMapping
- 欢迎页的HandlerMapping
- 还有我们系统兼容的BeanNameUrlHandlerMapping、RouterFunctionMapping和SimpleUrlHandlerMapping
- 一句话: 我们需要自定义的映射处理,我们也可以自己在容器中放HandlerMapping(就是来保存一个请求谁来处理,甚至于是发一个(我们经常自定义handlerMappingapi/v1/user和api/v2/user(v2版本获取用户)不一样,v1版本调用哪个v2版本调用哪个)这就可能不止止是Controller的变化,我们希望能自定义handlerMapping的规则。如果是v1版本,所有的请求,比如去哪个包里找;如果是v2版本给我去哪个包里找。这样就会非常方便)自定义handlerMapping
到此这篇关于Spring的请求映射handlerMapping以及原理详解的文章就介绍到这了,更多相关Spring的请求映射handlerMapping内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
解决spring-boot-maven-plugin报红的问题
这篇文章主要给大家介绍一下如何解决spring-boot-maven-plugin报红的问题,文中通过图文讲解的非常详细,具有一定的参考价值,需要的朋友可以参考下2023-08-08在win10系统下,如何配置Spring Cloud alibaba Seata以及出现问题时怎么解决
今天教大家如何在win10系统下,配置Spring Cloud alibaba Seata以及出现问题时怎么解决,文中有非常详细的介绍及代码示例,需要的朋友可以参考下2021-06-06spring boot之使用spring data jpa的自定义sql方式
这篇文章主要介绍了spring boot之使用spring data jpa的自定义sql方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2021-12-12
最新评论