1. spring mvc 剖析 -HandlerMapping

前言

使用spring mvc的时候,我们可以通过继承Controller或者注解的方式进行配置,那如何正确的找到真正处理的对象呢,这也就是本章我们要了解的内容 - HandlerMapping.

spring mvc流程图

此图来源: https://www.cnblogs.com/fangjian0423/p/springMVC-directory-summary.html

源码分析

HandlerMapping在spring mvc中所处的位置

正如我们所知道的,DispatcherServlet是spring mvc的入口方法,我们看一下源码:

public class DispatcherServlet extends FrameworkServlet {
    @Override
    protected void onRefresh(ApplicationContext context) {
        initStrategies(context);
    }
    protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }
      //其他代码忽略
}  

通过以上代码,可以看出来,当项目启动时,会对初始化很多策略,其中initHandlerMappings(context)是对于HandlerMapping的初始化。

在看HandlerMapping在DispatcherServlet的使用位置:

public class DispatcherServlet extends FrameworkServlet {
    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //其他代码忽略
        try {
            doDispatch(request, response);
        }
        finally {
            //其他代码忽略
        }
    }
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;

//去掉了try catch以及一些验证的代码

                //其他代码忽略
                mappedHandler = getHandler(processedRequest);
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                applyDefaultViewName(processedRequest, mv);
    }
}

上述代码中,mappedHandler = getHandler(processedRequest);是我们关注的,这个代码通过请求(HttpServeltRequest)找到对应的HandlerMapping,从而通过HandlerMapping拿到处理器Handler。

后边的逻辑为,根据Handler找到不同的适配器处理器(HandlerAdapter),处理后进行视图解析最后返回给用户。

HandlerMapping初始化

HandlerMapping

public interface HandlerMapping {
        //常量忽略
    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

通过HandlerMapping可以获取具体的HandlerExecutionChain(一个Handler的包装类型)

public class HandlerExecutionChain {
    private final Object handler;
    private HandlerInterceptor[] interceptors;
    private List<HandlerInterceptor> interceptorList;
    private int interceptorIndex = -1;

    public HandlerExecutionChain(Object handler) {
        this(handler, (HandlerInterceptor[]) null);
    }

    public HandlerExecutionChain(Object handler, HandlerInterceptor... interceptors) {
        if (handler instanceof HandlerExecutionChain) {
            HandlerExecutionChain originalChain = (HandlerExecutionChain) handler;
            this.handler = originalChain.getHandler();
            this.interceptorList = new ArrayList<HandlerInterceptor>();
            CollectionUtils.mergeArrayIntoCollection(originalChain.getInterceptors(), this.interceptorList);
            CollectionUtils.mergeArrayIntoCollection(interceptors, this.interceptorList);
        }
        else {
            this.handler = handler;
            this.interceptors = interceptors;
        }
    }

    public Object getHandler() {
        return this.handler;
    }
//其他代码忽略

其中Handler为真正的处理器 (比如,如果你继承了Controller,那么一个Controller就是一个Handler,如果你使用了注解,那么一个方法就是一个Handler)。

那么其他的interceptors或者interceptorList则是Spring mvc的拦截器,这些拦截器可以在Handler进行处理业务逻辑的前后,进行前置处理或者后置处理。

所以HandlerExecutionChain是一个带有拦截器的Handler包装类。

initHandlerMappings

public class DispatcherServlet extends FrameworkServlet {
private boolean detectAllHandlerMappings = true;//是否查询所有的HandlerMappings,如果此配置为false,那么必须找到名称为handlerMapping的HandlerMapping.

  private void initHandlerMappings(ApplicationContext context) {
        this.handlerMappings = null;

        if (this.detectAllHandlerMappings) {
            // 在上下文中查找所有的HandlerMapping
            Map<String, HandlerMapping> matchingBeans =
                    BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
            if (!matchingBeans.isEmpty()) {
                this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
                // 对所有的HandlerMapping进行排序
                AnnotationAwareOrderComparator.sort(this.handlerMappings);
            }
        }
        else {
            //忽略
        }

        //经过前两步,都没有HandlerMappings 那么启动一个默认的策略。
        if (this.handlerMappings == null) {
            this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
            //忽略
        }
    }
}

也就是说我们可以注册HandlerMapping,也可以使用默认策略,这步理论上讲,应该是不会报错的。

当我们启动spring boot的时候,我们看到如下:


spring默认加载的HandlerMapping

经过排序后,整个HandlerMapping的顺序如下:

  1. = {SimpleUrlHandlerMapping@7073}
  2. = {RequestMappingHandlerMapping@7063}
  3. = {BeanNameUrlHandlerMapping@7067}
  4. = {SimpleUrlHandlerMapping@7069}
  5. = {WebMvcConfigurationSupport$EmptyHandlerMapping@7065}
  6. = {WebMvcConfigurationSupport$EmptyHandlerMapping@7071}
  7. = {WebMvcAutoConfiguration$WelcomePageHandlerMapping@7075}

使用案例

    @Bean
    public SimpleUrlHandlerMapping mySimpleUrlHandlerMapping(){
        SimpleUrlHandlerMapping simpleUrlHandlerMapping = new SimpleUrlHandlerMapping();
        simpleUrlHandlerMapping.setUrlMap(ImmutableMap.of("/lrwin",new TestController()));
        simpleUrlHandlerMapping.setOrder(Integer.MIN_VALUE);
        return simpleUrlHandlerMapping;
    }
    

    public class TestController extends AbstractController{
        @Override
        protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {

            ModelAndView modelAndView = new ModelAndView();
            System.out.println("aaa");
            return modelAndView;
        }
    }

当访问http://localhost:8080/lrwin的时候,可以看到控制台输出了aaa.

SimpleUrlHandlerMapping的主要作用是可以给Controller取个访问路径别名,然后进行访问,可以找到相关联的Controller。

    @Component("/lrwinx")
    public class TestController extends AbstractController{
        @Override
        protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {

            ModelAndView modelAndView = new ModelAndView();
            System.out.println("aaa");
            return modelAndView;
        }
    }

当访问http://localhost:8080/lrwinx的时候,同样可以看到aaa

上边使用的HandlerMapping为BeanNameUrlHandlerMapping,它可以通过bean的名称来查询响应的Controller.

上边所述的Controller都是处理器(Handler).

还记得HandlerExecutionChain是Handler的一种包装类型吧。 HandlerMapping接口定义了返回这样的类型。

除了上述的两个实例以外,还有一种是注解的方式,注解方式中的每一个带有@RequestMapping的方法都是一个Handler. 这个复杂度比较高,我们最后再说。

类图层次

HandlerMapping类图结构

AbstractHandlerMapping

AbstractHandlerMapping结构图

看代码:

public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered {
        @Override
    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 = getApplicationContext().getBean(handlerName);
        }

        HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
        if (CorsUtils.isCorsRequest(request)) {
            CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
            CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
            CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
            executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
        }
        return executionChain;
    }

    protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;
}

子类只需要告诉谁是Handler就可以,当然Handler可以为处理器,也可以是处理器的封装类型HandlerExecutionChain。

AbstractUrlHandlerMapping

根据URL查询Handler关键方法

因为此方法是根据URL查询Handler的类,所以它的重点方法是lookupHandler.

protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
        // handlerMap存储了了urlPath对应的Handler--是一个Map类型。此handlerMap在本类方法registerHandler中进行注册的。
        Object handler = this.handlerMap.get(urlPath);
        if (handler != null) {
            // 如果获取到的Handler是一个String类型,那么就要考虑它有可能是一个beanName
            if (handler instanceof String) {
                String handlerName = (String) handler;
                handler = getApplicationContext().getBean(handlerName);
            }
            validateHandler(handler, request);
                        //构建一个带有基础信息的HandlerExecutionChain(Handler的封装类型)
            return buildPathExposingHandler(handler, urlPath, urlPath, null);
        }
        // 如果不是直接匹配的,那么是不是有可能是AntPathMatcher匹配的,当然,AntPathMatcher这种匹配,有可能会有多个,所以使用了List进行了临时存储
        List<String> matchingPatterns = new ArrayList<String>();
        for (String registeredPattern : this.handlerMap.keySet()) {
            if (getPathMatcher().match(registeredPattern, urlPath)) {
                matchingPatterns.add(registeredPattern);
            }
            else if (useTrailingSlashMatch()) {
                if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) {
                    matchingPatterns.add(registeredPattern +"/");
                }
            }
        }
//经过AntPathMatcher排序方式,获取一个最佳的匹配路径(也就是匹配路径List排序后的第一个)
        String bestPatternMatch = null;
        Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
        if (!matchingPatterns.isEmpty()) {
            Collections.sort(matchingPatterns, patternComparator);
            if (logger.isDebugEnabled()) {
                logger.debug("Matching patterns for request [" + urlPath + "] are " + matchingPatterns);
            }
            bestPatternMatch = matchingPatterns.get(0);
        }
//再通过最佳路径去halerMap中去查找对应的Handler
        if (bestPatternMatch != null) {
            handler = this.handlerMap.get(bestPatternMatch);
            if (handler == null) {
                Assert.isTrue(bestPatternMatch.endsWith("/"));
                handler = this.handlerMap.get(bestPatternMatch.substring(0, bestPatternMatch.length() - 1));
            }
            // Bean name or resolved handler?
            if (handler instanceof String) {
                String handlerName = (String) handler;
                handler = getApplicationContext().getBean(handlerName);
            }
            validateHandler(handler, request);
            String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestPatternMatch, urlPath);

            // There might be multiple 'best patterns', let's make sure we have the correct URI template variables
            // for all of them
            Map<String, String> uriTemplateVariables = new LinkedHashMap<String, String>();
            for (String matchingPattern : matchingPatterns) {
                if (patternComparator.compare(bestPatternMatch, matchingPattern) == 0) {
                    Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);
                    Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
                    uriTemplateVariables.putAll(decodedVars);
                }
            }
            if (logger.isDebugEnabled()) {
                logger.debug("URI Template variables for request [" + urlPath + "] are " + uriTemplateVariables);
            }
            return buildPathExposingHandler(handler, bestPatternMatch, pathWithinMapping, uriTemplateVariables);
        }
        // No handler found...
        return null;
    }

为了更深入的了解,我们关注一下排序算法:

Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
            Collections.sort(matchingPatterns, patternComparator);

getPathMatcher()是一个AntPathMatcher. 看一下getPatternComparator这个方法:

@Override
    public Comparator<String> getPatternComparator(String path) {
        return new AntPatternComparator(path);
    }

AntPatternComparator 会对符合ant表达式的url进行排序。
排序顺序:

  1. if it's null or a capture all pattern (i.e. it is equal to "/**")
  2. if the other pattern is an actual match
  3. if it's a catch-all pattern (i.e. it ends with "**"
  4. if it's got more "*" than the other pattern
  5. if it's got more "{foo}" than the other pattern
  6. if it's shorter than the other pattern

再来看一下,如何注册Handler到handlerMap中的。

protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
//一群验证和闲杂逻辑
                this.handlerMap.put(urlPath, resolvedHandler);
}

这样的话,我们得知,AbstractUrlHandlerMapping其实已经完成了注册Handler,和通过URL查找可用Handler的逻辑,它的子类只需要在合适的时机,调用注册方法就可以。我们来看他的子类:SimpleUrlHandlerMapping

SimpleUrlHandlerMapping

public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
    private final Map<String, Object> urlMap = new LinkedHashMap<String, Object>();
  
       //可以使用 properties配置,然后添加到urlMap中
    public void setMappings(Properties mappings) {
        CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
    }
//设置urlMap中
    public void setUrlMap(Map<String, ?> urlMap) {
        this.urlMap.putAll(urlMap);
    }
    public Map<String, ?> getUrlMap() {
        return this.urlMap;
    }

//初始化时,注册所有的Handler
    @Override
    public void initApplicationContext() throws BeansException {
        super.initApplicationContext();
        registerHandlers(this.urlMap);
    }

//调用父类注册方法registerHandler的逻辑
    protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
        if (urlMap.isEmpty()) {
            logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping");
        }
        else {
            for (Map.Entry<String, Object> entry : urlMap.entrySet()) {
                String url = entry.getKey();
                Object handler = entry.getValue();
                if (!url.startsWith("/")) {
                    url = "/" + url;
                }
                if (handler instanceof String) {
                    handler = ((String) handler).trim();
                }
                registerHandler(url, handler);
            }
        }
    }
}

小结

  1. HandlerMapping

定义了需要返回handler的包装类型HandlerExecutionChain的接口 : getHandler

2.AbstractHandlerMapping

封装了getHandler,并且封装了拦截器。子类需要提供一下Handler

protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;

3.AbstractUrlHandlerMapping
实现了getHandlerInternal,提供了如何通过URL查找Handler的方法,核心逻辑方法:

protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {

提供了注册方法,子类需要调用注册方法:

protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException 

4.SimpleUrlHandlerMapping
仅仅是解析Properties或者Map数据,然后在初始化的时候调用父类的registerHandler进行注册。

牛逼的注解HanlderMapping

RequestMappingHandlerMapping类图

AbstractHandlerMethodMapping

这是一个以方法为Handler的抽象类:

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception

看一下HandlerMethod:

public class HandlerMethod {
    private final Object bean;//类对象
    private final Class<?> beanType;//对象的Class
    private final Method method;//具体方法
    private final Method bridgedMethod;//桥接方法(桥接方法以后再将)
    private final MethodParameter[] parameters;//方法参数
    private final HandlerMethod resolvedFromHandlerMethod;//其他Handler的方法
//...
}

这个HandlerMethod就是一个方法的描述类。

@Override
    protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
        //...
            HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
            //...
    }

核心方法lookupHandlerMethod:

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
//找到根据url找到所有对应的Match(这个对象是HandlerMethod的包装类型,它包含HandlerMethod和他的mapping)
        List<Match> matches = new ArrayList<Match>();
        List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
        if (directPathMatches != null) {
            addMatchingMappings(directPathMatches, matches, request);
        }
        if (matches.isEmpty()) {
            // No choice but to go through all mappings...
            addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
        }
//从Match集合中,找到最佳的一个
        if (!matches.isEmpty()) {
            Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
            Collections.sort(matches, comparator);
            if (logger.isTraceEnabled()) {
                logger.trace("Found " + matches.size() + " matching mapping(s) for [" +
                        lookupPath + "] : " + matches);
            }
            Match bestMatch = matches.get(0);
            if (matches.size() > 1) {
                if (CorsUtils.isPreFlightRequest(request)) {
                    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();
                    throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
                            request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
                }
            }
            handleMatch(bestMatch.mapping, lookupPath, request);
            return bestMatch.handlerMethod;
        }
        else {
            return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
        }
    }

对于AbstractHandlerMethodMapping,有几个概念:

mapping:处理方法的mapping(后边会讲到,是一个RequestMappingInfo)

handler:处理器(只一个类)
method:执行方法(类的执行方法)

HandlerMethod: 可以理解成一个handler和method的一个封装。

Match: mapping和HandlerMethod的一个封装。

1.根据 url找到所有的mapping:
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
2.封装成找到对应的Match:
T match = getMatchingMapping(mapping, request);

protected abstract T getMatchingMapping(T mapping, HttpServletRequest request);

这个要由子类实现
3.加入集合,然后找到最佳Match

RequestMappingInfoHandlerMapping

RequestMappingInfo:


public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
    private final String name;
    private final PatternsRequestCondition patternsCondition;
    private final RequestMethodsRequestCondition methodsCondition;
    private final ParamsRequestCondition paramsCondition;
    private final HeadersRequestCondition headersCondition;
    private final ConsumesRequestCondition consumesCondition;
    private final ProducesRequestCondition producesCondition;
    private final RequestConditionHolder customConditionHolder;
//....
}

这个RequestMappingInfo类,其实和@RequestMapping里的属性是一一对应的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 22,310评论 1 92
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,747评论 6 342
  • 引言 一直以来都在使用Spring mvc,能够熟练使用它的各种组件。但是,它一直像个黑盒一样,我并不知道它内部是...
    yoqu阅读 905评论 0 24
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • Spring MVC一、什么是 Spring MVCSpring MVC 属于 SpringFrameWork 的...
    任任任任师艳阅读 3,372评论 0 32