Spring MVC 基礎(chǔ)源碼學(xué)習(xí)

spring MVC是嚴(yán)格遵守java servlet規(guī)范的一種web框架筷弦,可以打包成war包交由web容器(Tomcat或者jetty)運(yùn)行坐漏。接下來(lái)就來(lái)學(xué)習(xí)下spring mvc的運(yùn)行過(guò)程以及其中的細(xì)節(jié),如何和Tomcat無(wú)縫合作薄货,如何和spring 本身的核心功能IOC、AOP合作。

MVC 實(shí)例

接下來(lái)就搭建一個(gè)基于maven的spring mvc的實(shí)例蝇狼。

項(xiàng)目結(jié)構(gòu)


image.png

StudentController 類(lèi)

@Controller
@RequestMapping
public class StudentController {

    @RequestMapping(value = "/h")
    @ResponseBody
    public String getStudentInfo() {
        return "hello world";
    }
}

web.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/applicationContext.xml</param-value>
    </context-param>
    
    <servlet>
        <servlet-name>demo</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>WEB-INF/applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>demo</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>

applicationContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:component-scan base-package="com.demo.web" />

</beans>

pom.xml

// 插件部分
<build>
   <finalName>demo</finalName>
   <plugins>
       <plugin>
           <groupId>org.apache.tomcat.maven</groupId>
           <artifactId>tomcat7-maven-plugin</artifactId>
           <version>2.2</version>
           <configuration>
               <path>/</path>
               <uriEncoding>${file_encoding}</uriEncoding>
               <port>9912</port>
               <server>tomcat7</server>
           </configuration>
       </plugin>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-war-plugin</artifactId>
           <version>2.5</version>
       </plugin>


       <!-- Java Compiler -->
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.3</version>
           <configuration>
               <source>${java_source_version}</source>
               <target>${java_target_version}</target>
               <encoding>${file_encoding}</encoding>
           </configuration>
       </plugin>
   </plugins>
   <resources>
       <resource>
           <directory>src/main/resources</directory>
       </resource>
   </resources>

</build>

然后通過(guò)mvn tomcat7:run就可以正常啟動(dòng)了

Tomcat 基礎(chǔ)

在介紹spring mvc的工作原理之前,有必要介紹下web容器的一種Tomcat倡怎。Tomcat是一個(gè)開(kāi)源的web容器迅耘,嚴(yán)格準(zhǔn)守servlet規(guī)范,在Tomcat中包含了各種各樣的組件诈胜,層層嵌套依賴(lài)豹障,如下圖所示。

image.png

catalina是最核心的組件焦匈,也可以認(rèn)為T(mén)omcat是從它開(kāi)始啟動(dòng)的血公,他持有1個(gè)server容器,server容器可以包含了多個(gè)service容器缓熟,每個(gè)service容器都持有了一個(gè)connector連接器以及一個(gè)engine累魔,engine有層層包含了host、context够滑、wrapper等垦写。

其中engine、host彰触、context梯投、wrapper又分別存在各自的一個(gè)管道pipeline以及至少一個(gè)閥門(mén)valve,閥門(mén)可以為request和response添加任何外置的功能。

Tomcat啟動(dòng)是由各自容器的監(jiān)聽(tīng)器調(diào)用啟動(dòng)的分蓖,按照上面所說(shuō)的順序依次執(zhí)行啟動(dòng)的尔艇。

那我們常用的servlet是在哪里的么?他是被wrapper包裝的么鹤,每一個(gè)wrapper持有一個(gè)servlet,所以在xml中配置了幾個(gè)servlet终娃,則就會(huì)存在多少個(gè)wrapper。并且servlet是通過(guò)ServletContext傳遞上下文的蒸甜。在具體的URL映射的時(shí)候棠耕,會(huì)先根據(jù)各自的servlet的URL配置在Tomcat的mappingdata中體現(xiàn),經(jīng)過(guò)host柠新、context窍荧、再到選擇不同類(lèi)型的wrapper(包含了wildcardWrappers、extensionWrappers登颓、defaultWrapper搅荞、exactWrappers四種wrapper類(lèi)型)最后才具體到某一個(gè)servlet請(qǐng)求上。

DispatcherServlet init 過(guò)程

DispatcherServlet類(lèi)是spring mvc中實(shí)現(xiàn)的了servlet規(guī)范的實(shí)體類(lèi)框咙,實(shí)現(xiàn)了HttpServlet類(lèi)咕痛,如下圖

image.png

首先需要明確一點(diǎn)的是,DispatcherServlet也只是一個(gè)HttpServlet類(lèi)喇嘱,他是被wrapper調(diào)用的init(ServletConfig config)方法進(jìn)入的茉贡,設(shè)置好config之后,進(jìn)入到init()方法者铜。

PS:在當(dāng)前環(huán)境中腔丧,ServletConfig是StandardWrapperFacade類(lèi),可以從中獲取到在xml配置的例如contextConfigLocation數(shù)據(jù)

HttpServletBean 類(lèi)

    public final void init() throws ServletException {
        if (logger.isDebugEnabled()) {
            logger.debug("Initializing servlet '" + getServletName() + "'");
        }

        // Set bean properties from init parameters.
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        // 獲取xml配置的屬性作烟,在下面貼的圖片中可以看到愉粤,當(dāng)DispatcherServlet沒(méi)有任何配置的時(shí)候,就會(huì)拋出異常拿撩,說(shuō)缺少必備的配置屬性
        if (!pvs.isEmpty()) {
            try {
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
                initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
                // 這一步就是設(shè)置DispatcherServlet的屬性值的操作
            }
            catch (BeansException ex) {
                if (logger.isErrorEnabled()) {
                    logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
                }
                throw ex;
            }
        }

        // 開(kāi)始真正的初始化DispatcherServlet類(lèi)了
        initServletBean();

        if (logger.isDebugEnabled()) {
            logger.debug("Servlet '" + getServletName() + "' configured successfully");
        }
    }
    
image.png

FrameworkServlet 類(lèi)

    protected final void initServletBean() throws ServletException {
        try {
            this.webApplicationContext = initWebApplicationContext();
            // 這個(gè)是初始化當(dāng)前dispatchservlet的webApplicationContext參數(shù)
            // 然后該參數(shù)中會(huì)持有spring的IOC容器等信息衣厘,通過(guò)這個(gè)參數(shù)可以獲取bean數(shù)據(jù)
            // 具體細(xì)節(jié)可看下面的代碼
            initFrameworkServlet();
            // 未做任何事情,由子類(lèi)實(shí)現(xiàn)压恒,不過(guò)暫時(shí)為空
        }
        catch (ServletException ex) {
            this.logger.error("Context initialization failed", ex);
            throw ex;
        }
        catch (RuntimeException ex) {
            this.logger.error("Context initialization failed", ex);
            throw ex;
        }
    }
    
    protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        // 從當(dāng)前的上下文中查找是否存在跟上下文信息(通過(guò)ContextLoaderListener加載的都會(huì)存在的)
        WebApplicationContext wac = null;
        if (this.webApplicationContext != null) {
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                        cwac.setParent(rootContext);
                    }
                    // 重新再刷新一次WebApplicationContext持有的內(nèi)容信息
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
        if (wac == null) {
            wac = findWebApplicationContext();
        }
        if (wac == null) {
            // 創(chuàng)建一個(gè)新的WebApplicationContext信息影暴,并設(shè)置好其parent
            // 這里需要好好看看,同樣的拆分為兩部分執(zhí)行
            // 1探赫、創(chuàng)建WebApplicationContext類(lèi)型宙,也就是XmlWebApplicationContext實(shí)體類(lèi),并設(shè)置好其環(huán)境伦吠、配置屬性妆兑、父類(lèi)等信息
            // 2魂拦、調(diào)用configureAndRefreshWebApplicationContext,同樣的設(shè)置好其關(guān)于servlet的上下文的屬性信息箭跳,最后調(diào)用wac.refresh()
            // PS: refresh()就開(kāi)始了spring IOC的解析存儲(chǔ)操作了
            wac = createWebApplicationContext(rootContext);
            // 不過(guò)這里有一步需要注意到晨另,在refresh()中,最后會(huì)有onApplicationEvent()的操作谱姓,他會(huì)調(diào)用在DispatcherServlet類(lèi)的initStrategies方法,完成URL映射刨晴、Template等操作
            // 并且設(shè)置this.refreshEventReceived為true
        }

        if (!this.refreshEventReceived) {
            // 完成了上面的刷新操作就不要再刷新了
            // 這里的onRefresh還是會(huì)調(diào)用initStrategies方法
            // 殊途同歸罷了
            onRefresh(wac);
        }

        if (this.publishContext) {
            // 把當(dāng)前的上下文信息也保存到ServletContext中
            // 如果注意到的函數(shù)開(kāi)頭的rootContext的獲取方法會(huì)發(fā)現(xiàn)也是通過(guò)這樣的方式獲取的
            String attrName = getServletContextAttributeName();
            getServletContext().setAttribute(attrName, wac);
        }
        return wac;
    }

這樣就完成了DispatcherServlet的初始化操作了屉来,接下來(lái)就可以具體處理http請(qǐng)求了,當(dāng)前這其中遺漏了很多重要的的點(diǎn)狈癞,例如

  • rootContext 這是怎么一回事茄靠,是必須的么,和applicationContext.xml又有什么關(guān)系呢蝶桶?
  • xml配置的context-param和servlet的init-param有什么區(qū)別慨绳?

這幾個(gè)點(diǎn)會(huì)在后續(xù)的學(xué)習(xí)筆記中再了解其原理,當(dāng)前主要是介紹DispatcherServlet以及相關(guān)的東西真竖。

DispatcherServlet URL映射以及請(qǐng)求處理 過(guò)程

Tomcat會(huì)通過(guò)的DispatcherServlet的servlet-mapping的屬性匹配到合適的wrapper脐雪,再關(guān)聯(lián)到具體的DispatcherServlet,也就意味著在web.xml確實(shí)可以配置多個(gè)servlet恢共,只是在spring mvc中常用的就這一個(gè)而已战秋。

HTTP請(qǐng)求,最后都會(huì)打到DispatcherServlet類(lèi)的doService方法中讨韭,設(shè)置一些屬性之后脂信,又來(lái)到了doDispatch方法中,這個(gè)方法是核心也是最重要的http請(qǐng)求處理的方法,通過(guò)URL選擇合適的controller透硝,選擇具體的modelandview狰闪,再渲染生成數(shù)據(jù)回調(diào)等等操作

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            ModelAndView mv = null;
            Exception dispatchException = null;

            try {
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);

                mappedHandler = getHandler(processedRequest);
                // 通過(guò)URL找到合適的controller,并存儲(chǔ)在HandlerExecutionChain對(duì)著中
                // 還會(huì)檢測(cè)是否進(jìn)行cors跨域操作,如果存在跨域就會(huì)按照跨域的要求去處理
                // @CrossOrigin(origins="http://test.com") 可以直接放在controller的注解上
                if (mappedHandler == null || mappedHandler.getHandler() == null) {
                   // 沒(méi)有找到合適的處理handle濒生,就是404了
                   // 這就可以自定義配置404頁(yè)面以及跳轉(zhuǎn)等信息
                   // response.sendError(HttpServletResponse.SC_NOT_FOUND)
                   // 這里又可以引出一個(gè)問(wèn)題了埋泵,如何配置404頁(yè)面
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // 檢測(cè)當(dāng)前獲取的controller是否合適,并且得到合適的HandlerAdapter
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // 如果符合可以使用緩存機(jī)制,減少不必要的請(qǐng)求
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                   // 使用spring本身的攔截器前置處理
                    return;
                }

                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                // 真正的處理請(qǐng)求甜攀,在本demo中會(huì)調(diào)到AnnotationMethodHandlerAdapter類(lèi)中執(zhí)行handle方法秋泄,返回的mv是ModelAndView
                // 解析當(dāng)前handler中包含了所有的方法,匹配其中合適的方法之后规阀,invoke調(diào)用
                // 不過(guò)這里需要注意到恒序,類(lèi)似于返回json的請(qǐng)求,是不需要模板渲染的谁撼,此時(shí)mv返回的是null歧胁,不過(guò)具體的json數(shù)據(jù)已經(jīng)填入到了responseBody中

                applyDefaultViewName(processedRequest, mv);
                // 如果mv
                mappedHandler.applyPostHandle(processedRequest, response, mv);
                // spring 攔截器的后置處理
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            catch (Throwable err) {
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }
            // 處理結(jié)果了滋饲,如果存在異常會(huì)把exception帶上,例如500錯(cuò)誤等喊巍,按照異常處理
            // 如果存在了模板屠缭,需要經(jīng)過(guò)render處理
            // 否則就直接把得到的數(shù)據(jù)當(dāng)做body返回
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Throwable err) {
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                    new NestedServletException("Handler processing failed", err));
        }
        finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                // Instead of postHandle and afterCompletion
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            }
            else {
                // Clean up any resources used by a multipart request.
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }

到這里就完成了對(duì)一個(gè)普通的http請(qǐng)求的處理全過(guò)程,不過(guò)還是存在諸多問(wèn)題沒(méi)有去分析崭参,如圖

  • cors跨域是什么搭伤,如何使用
  • URL映射規(guī)則是如何完成的,以及和Tomcat的URL映射有什么關(guān)聯(lián)么退盯?
  • 模板是如何被渲染的婆赠,在xml中如何設(shè)置不同的模板的?

還有個(gè)問(wèn)題一直疏忽了海洼,在spring mvc中存在大量的if(***.debug)這種操作跨新,那么該如何配置使用日志系統(tǒng)呢?

接下來(lái)會(huì)再寫(xiě)筆記去分析上述提到的各種問(wèn)題~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末坏逢,一起剝皮案震驚了整個(gè)濱河市域帐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌是整,老刑警劉巖肖揣,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異贰盗,居然都是意外死亡许饿,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)舵盈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)陋率,“玉大人,你說(shuō)我怎么就攤上這事秽晚⊥咴悖” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵赴蝇,是天一觀(guān)的道長(zhǎng)菩浙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)句伶,這世上最難降的妖魔是什么劲蜻? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮考余,結(jié)果婚禮上先嬉,老公的妹妹穿的比我還像新娘。我一直安慰自己楚堤,他們只是感情好疫蔓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布含懊。 她就那樣靜靜地躺著,像睡著了一般衅胀。 火紅的嫁衣襯著肌膚如雪岔乔。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,521評(píng)論 1 304
  • 那天滚躯,我揣著相機(jī)與錄音雏门,去河邊找鬼。 笑死哀九,一個(gè)胖子當(dāng)著我的面吹牛剿配,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播阅束,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼茄唐!你這毒婦竟也來(lái)了息裸?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤沪编,失蹤者是張志新(化名)和其女友劉穎呼盆,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蚁廓,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡访圃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了相嵌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腿时。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖饭宾,靈堂內(nèi)的尸體忽然破棺而出批糟,到底是詐尸還是另有隱情,我是刑警寧澤看铆,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布徽鼎,位于F島的核電站,受9級(jí)特大地震影響弹惦,放射性物質(zhì)發(fā)生泄漏否淤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一棠隐、第九天 我趴在偏房一處隱蔽的房頂上張望石抡。 院中可真熱鬧,春花似錦宵荒、人聲如沸汁雷。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)侠讯。三九已至挖藏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間厢漩,已是汗流浹背膜眠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留溜嗜,地道東北人宵膨。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像炸宵,于是被迫代替她去往敵國(guó)和親辟躏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,811評(píng)論 6 342
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理土全,服務(wù)發(fā)現(xiàn)捎琐,斷路器,智...
    卡卡羅2017閱讀 134,657評(píng)論 18 139
  • 從三月份找實(shí)習(xí)到現(xiàn)在裹匙,面了一些公司瑞凑,掛了不少,但最終還是拿到小米概页、百度籽御、阿里、京東惰匙、新浪技掏、CVTE、樂(lè)視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,246評(píng)論 11 349
  • 本章主要內(nèi)容: 備用的Spring MVC配置項(xiàng) 處理文件上傳 控制器中的異常處理 使用flash屬性 “等等徽曲,客...
    hoxis閱讀 3,522評(píng)論 1 26
  • 看進(jìn)陰影里面 ——裸體的邊緣 看到了嗎零截?在那里 另一個(gè)你 很美好,太孤單
    我是蝎大人閱讀 185評(píng)論 2 3