從multipartResolver的一個異常到multipartResolver源碼分析

記錄一下前段時間遇到的一個關(guān)于multipartResolver的異常以躯,以及后面找出原因的過程。

異常分析

異常如下:

2018-01-22 18:05:38.041 ERROR com.exception.ExceptionHandler.resolveException:22 -Could not Q multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. null
org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. null
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:165) ~[spring-web-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:142) ~[spring-web-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1089) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:928) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:968) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:870) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:661) [servlet-api.jar:na]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:844) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) [servlet-api.jar:na]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [catalina.jar:8.5.24]
    at org.apache.catalina.core.A

這個異常大意是說multipart/form-data傳輸?shù)谋韱未嬖诳罩岛枧В瑳]有辦法從request的表單中讀到某個值于购。

確定了請求本身非空值之后,去看看是不是SpringMVC接收請求并從請求中讀出參數(shù)的過程中出了問題篓叶。

那么烈掠,SpringMVC是如何處理請求傳過來的文件的呢?

multipartResolver處理請求的過程

DispatcherServlet轉(zhuǎn)發(fā)

首先缸托,Spring提供了對文件多路上傳的支持左敌,只要注冊一個名為"multipartResolver"的bean,那么后續(xù)SpringMVC的DispatcherServlet在接收到請求的時候俐镐,會判斷請求是不是multipart文件矫限。
如果是的話,就會調(diào)用"multipartResolver"佩抹,將請求包裝成一個MultipartHttpServletRequest對象叼风,然后后面就可以從這個對象中取出文件來進行處理了。

multipartResolver的裝載

Spring提供了一個對于MultipartResolver接口的實現(xiàn):org.springframework.web.multipart.commons.CommonsMultipartResolver棍苹∥匏蓿看一下源碼:

public class CommonsMultipartResolver extends CommonsFileUploadSupport
        implements MultipartResolver, ServletContextAware {
...
}

CommonsFileUploadSupport是對于XML配置"multipartResolver"時的支持。
在XML配置multipartResolver時的配置如下:

<bean id="multipartResolver"
             class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
              <!-- 默認編碼 -->
              <property name="defaultEncoding" value="utf-8" />
              <!-- 設(shè)置multipart請求所允許的最大大小枢里,默認不限制 -->
              <property name="maxUploadSize" value="10485760000" />
              <!-- 設(shè)置一個大小孽鸡,multipart請求小于這個大小時會存到內(nèi)存中,大于這個內(nèi)存會存到硬盤中 -->
              <property name="maxInMemorySize" value="40960" />
       </bean>

這些property配置會被加載到CommonsFileUploadSupport中栏豺,然后被CommonsMultipartResolver繼承彬碱。

CommonsMultipartResolver的處理過程

然后就是,其實CommonsMultipartResolver依賴于Apache的jar包來實現(xiàn):common-fileupload奥洼。

TIM截圖20180201193231.png

CommonsMultipartResolver接收到請求之后巷疼,是這樣對HttpServletReques進行處理的:

(CommonsMultipartResolver文件)

@Override
    public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
        Assert.notNull(request, "Request must not be null");
        //懶加載
        if (this.resolveLazily) {
            return new DefaultMultipartHttpServletRequest(request) {
                @Override
                protected void initializeMultipart() {
                    MultipartParsingResult parsingResult = parseRequest(request);
                    setMultipartFiles(parsingResult.getMultipartFiles());
                    setMultipartParameters(parsingResult.getMultipartParameters());
                    setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
                }
            };
        }
        else {
             //這里對request進行了解析
            MultipartParsingResult parsingResult = parseRequest(request);
            return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
                    parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
        }
    }

this.resolveLazily是懶加載,如果為true灵奖,會在initializeMultipart()被調(diào)用嚼沿,即發(fā)起文檔信息獲取的時候,才去封裝DefaultMultipartHttpServletRequest桑寨;如果為false伏尼,立即封裝DefaultMultipartHttpServletRequest忿檩。

resolveLazily默認為false尉尾。

然后再去看一下parseRequest(request)的解析:

(CommonsMultipartResolver文件)

    /**
     * Parse the given servlet request, resolving its multipart elements.
     * 對servlet請求進行處理,轉(zhuǎn)成multipart結(jié)構(gòu)
     * @param request the request to parse
     * @return the parsing result
     * @throws MultipartException if multipart resolution failed.
     */
    protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
        //從請求中讀出這個請求的編碼
        String encoding = determineEncoding(request);
        //按照請求的編碼燥透,獲取一個FileUpload對象沙咏,裝載到CommonsFileUploadSupport的property屬性都會被裝入這個對象中
        //prepareFileUpload是繼承自CommonsFileUploadSupport的函數(shù),會比較請求的編碼和XML中配置的編碼班套,如果不一樣肢藐,會拒絕處理
        FileUpload fileUpload = prepareFileUpload(encoding);
        try {
            //對請求中的multipart文件進行具體的處理
            List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
            return parseFileItems(fileItems, encoding);
        }
        catch (FileUploadBase.SizeLimitExceededException ex) {
            throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
        }
        catch (FileUploadException ex) {
            throw new MultipartException("Could not parse multipart servlet request", ex);
        }
    }

上面的((ServletFileUpload) fileUpload).parseRequest(request)解析實現(xiàn)如下:

(FileUploadBase文件)

    /**
     * Processes an <a >RFC 1867</a>
     * compliant <code>multipart/form-data</code> stream.
     *
     * @param ctx The context for the request to be parsed.
     *
     * @return A list of <code>FileItem</code> instances parsed from the
     *         request, in the order that they were transmitted.
     *
     * @throws FileUploadException if there are problems reading/parsing
     *                             the request or storing files.
     */
    public List<FileItem> parseRequest(RequestContext ctx)
            throws FileUploadException {
        List<FileItem> items = new ArrayList<FileItem>();
        boolean successful = false;
        try {
            //從請求中取出multipart文件
            FileItemFactoryFactoryFactoryator iter = getItemIterator(ctx);
            //獲得FileItemFactory工廠,實現(xiàn)類為DiskFileItemFactory
            FileItemFactory fac = getFileItemFactory();
            if (fac == null) {
                throw new NullPointerException("No FileItemFactory has been set.");
            }
            while (iter.hasNext()) {
                final FileItemStream item = iter.next();
                // Don't use getName() here to prevent an InvalidFileNameException.
                final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;
                //工廠模式吱韭,獲取FileItem對象吆豹,實現(xiàn)類是DiskFileItem
                FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
                                                   item.isFormField(), fileName);
                items.add(fileItem);
                try {
                    Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
                } catch (FileUploadIOException e) {
                    throw (FileUploadException) e.getCause();
                } catch (IOException e) {
                    //我們遇到的異常就是在這里拋出的
                    throw new IOFileUploadException(format("Processing of %s request failed. %s",
                                                           MULTIPART_FORM_DATA, e.getMessage()), e);
                }
                final FileItemHeaders fih = item.getHeaders();
                fileItem.setHeaders(fih);
            }
            successful = true;
            return items;
        } catch (FileUploadIOException e) {
            throw (FileUploadException) e.getCause();
        } catch (IOException e) {
            throw new FileUploadException(e.getMessage(), e);
        } finally {
            if (!successful) {
                for (FileItem fileItem : items) {
                    try {
                        fileItem.delete();
                    } catch (Throwable e) {
                        // ignore it
                    }
                }
            }
        }
    }

我們遇到的異常就是在這個位置拋出的鱼的,后面找錯誤會在這里深入,但是我們還是先把整個請求流轉(zhuǎn)的流程走完痘煤。

到此凑阶,List<FileItem>對象就處理完返回了,然后再繼續(xù)看對List<FileItem>的處理

(CommonsFileUploadSupport文件)

    /**
     * Parse the given List of Commons FileItems into a Spring MultipartParsingResult,
     * containing Spring MultipartFile instances and a Map of multipart parameter.
     * @param fileItems the Commons FileIterms to parse
     * @param encoding the encoding to use for form fields
     * @return the Spring MultipartParsingResult
     * @see CommonsMultipartFile#CommonsMultipartFile(org.apache.commons.fileupload.FileItem)
     */
    protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
        MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<String, MultipartFile>();
        Map<String, String[]> multipartParameters = new HashMap<String, String[]>();
        Map<String, String> multipartParameterContentTypes = new HashMap<String, String>();

        // Extract multipart files and multipart parameters.
        for (FileItem fileItem : fileItems) {
            //如果fileItem是一個表單
            if (fileItem.isFormField()) {
                String value;
                String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
                if (partEncoding != null) {
                    try {
                        value = fileItem.getString(partEncoding);
                    }
                    catch (UnsupportedEncodingException ex) {
                        if (logger.isWarnEnabled()) {
                            logger.warn("Could not decode multipart item '" + fileItem.getFieldName() +
                                    "' with encoding '" + partEncoding + "': using platform default");
                        }
                        value = fileItem.getString();
                    }
                }
                else {
                    value = fileItem.getString();
                }
                String[] curParam = multipartParameters.get(fileItem.getFieldName());
                if (curParam == null) {
                    // simple form field
                    multipartParameters.put(fileItem.getFieldName(), new String[] {value});
                }
                else {
                    // array of simple form fields
                    String[] newParam = StringUtils.addStringToArray(curParam, value);
                    multipartParameters.put(fileItem.getFieldName(), newParam);
                }
                multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());
            }
            //如果fileItem是一個multipart文件
            else {
                // multipart file field
                CommonsMultipartFile file = new CommonsMultipartFile(fileItem);
                multipartFiles.add(file.getName(), file);
                if (logger.isDebugEnabled()) {
                    logger.debug("Found multipart file [" + file.getName() + "] of size " + file.getSize() +
                            " bytes with original filename [" + file.getOriginalFilename() + "], stored " +
                            file.getStorageDescription());
                }
            }
        }
        return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
    }

到此衷快,MultipartParsingResult的處理就結(jié)束并返回了宙橱,然后CommonsMultipartResolver中的resolveMultipart就將其裝到DefaultMultipartHttpServletRequest中并返回,處理完了蘸拔。

DefaultMultipartHttpServletRequest是MultipartHttpServletRequest的實現(xiàn)類师郑。

關(guān)于maxInMemorySize

前面已經(jīng)說過,maxInMemorySize的作用是“設(shè)置一個大小调窍,multipart請求小于這個大小時會存到內(nèi)存中宝冕,大于這個內(nèi)存會存到硬盤中”
再看一下maxInMemorySize被set到對象中的過程:

(CommonsFileUploadSupport文件)

    /**
     * Set the maximum allowed size (in bytes) before uploads are written to disk.
     * Uploaded files will still be received past this amount, but they will not be
     * stored in memory. Default is 10240, according to Commons FileUpload.
     * @param maxInMemorySize the maximum in memory size allowed
     * @see org.apache.commons.fileupload.disk.DiskFileItemFactory#setSizeThreshold
     */
    public void setMaxInMemorySize(int maxInMemorySize) {
        this.fileItemFactory.setSizeThreshold(maxInMemorySize);
    }

CommonsFileUploadSupport中有一個fileItemFactory對象陨晶,maxInMemorySize就被set到了這個工廠類的屬性SizeThreshold里猬仁。

這個fileItemFactory工廠類,會在生成fileItem對象的時候用到先誉。
生成這個對象的過程中湿刽,會根據(jù)maxInMemorySize來判斷,是將其存到內(nèi)存中褐耳,還是存到硬盤中诈闺。

存儲的過程在前面已經(jīng)提過了:

...
        try {
                Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
                } catch (FileUploadIOException e) {
                    throw (FileUploadException) e.getCause();
                } catch (IOException e) {
                    throw new IOFileUploadException(format("Processing of %s request failed. %s",
                                                           MULTIPART_FORM_DATA, e.getMessage()), e);
                }
                final FileItemHeaders fih = item.getHeaders();

進入fileItem.getOutputStream()看看:

    /**
     * Returns an {@link java.io.OutputStream OutputStream} that can
     * be used for storing the contents of the file.
     *
     * @return An {@link java.io.OutputStream OutputStream} that can be used
     *         for storing the contensts of the file.
     *
     * @throws IOException if an error occurs.
     */
    public OutputStream getOutputStream()
        throws IOException {
        if (dfos == null) {
            File outputFile = getTempFile();
            dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
        }
        return dfos;
    }

再進去getTempFile():

    /**
     * Creates and returns a {@link java.io.File File} representing a uniquely
     * named temporary file in the configured repository path. The lifetime of
     * the file is tied to the lifetime of the <code>FileItem</code> instance;
     * the file will be deleted when the instance is garbage collected.
     *
     * @return The {@link java.io.File File} to be used for temporary storage.
     */
    protected File getTempFile() {
        if (tempFile == null) {
            File tempDir = repository;
            if (tempDir == null) {
                tempDir = new File(System.getProperty("java.io.tmpdir"));
            }

            String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId());

            tempFile = new File(tempDir, tempFileName);
        }
        return tempFile;
    }

當(dāng)沒有設(shè)置uploadTempDir屬性,也就是FileItemFactory中的repository的時候會自動選擇一個緩存路徑System.getProperty("java.io.tmpdir")铃芦,將上傳請求落到硬盤上的這個位置雅镊。

注意看這個注釋:the file will be deleted when the instance is garbage collected. 這里說了FileItem的實例聲明周期,當(dāng)GC的時候刃滓,存在內(nèi)存里的FileItem會被GC回收掉仁烹。所以這就是為什么沒有辦法讀到multipart/form-data對象。

bug原因和解決方案

  1. 解決頻繁GC的問題咧虎。太過頻繁的GC明顯是出了問題了卓缰,導(dǎo)致請求中的文件被回收掉,報空指針砰诵。(這也是我這邊解決問題的方案)
  2. 設(shè)置好maxInMemorySize和uploadTempDir兩個屬性征唬,保證上傳文件緩存到硬盤上,普通請求在內(nèi)存中就可以了茁彭。如果涉及大量的文件上傳总寒,這個是很有必要的,不然并發(fā)高的時候理肺,內(nèi)存會被文件給占滿卑吭。然后會觸發(fā)GC,F(xiàn)ileItem被回收掉之后忧侧,后面就會再去讀取,就被出現(xiàn)我們異常中的空指針錯誤媳禁。
  3. 還有一種可能性,就是multipartResolver配置的時候画切,沒有設(shè)置uploadTempDir屬性竣稽。按理說這個是沒有問題的,因為會默認幫你設(shè)為系統(tǒng)的緩存路徑霍弹,這個路徑通常是/tmp毫别,這個目錄所有用戶都有權(quán)限讀取。但是如果是生產(chǎn)環(huán)境典格,這個系統(tǒng)默認的緩存路徑很可能會被修改過岛宦,修改了位置,或者權(quán)限耍缴。這也是為了安全的方面考慮砾肺,但是這在我們所講的流程中,就會造成后面讀取的時候防嗡,出現(xiàn)空指針的錯誤变汪。

這些異常都不容易排查,所以需要對整個流程都清晰了之后蚁趁,才容易找到問題的所在裙盾。單單看自己的代碼是不能看出來的,例如權(quán)限的問題他嫡,在實際生產(chǎn)環(huán)境中才會遇到番官,也比較無奈。

我為什么要把這個問題寫的這么復(fù)雜

把這個問題寫了這么多钢属,最后的解決方案卻寫的很少徘熔,看起來可能是很傻,但是是有原因的:

  1. 這個異常在網(wǎng)上沒有找到有用的解決方案淆党,也沒有看到講明白原因的
  2. 把流程理一遍酷师,有利于后面遇到類似問題的時候,更好地解決宁否。例如說不是報空值窒升,而是報其他問題的時候缀遍。
  3. 最重要的一點慕匠,由于運行環(huán)境的原因,沒有辦法復(fù)現(xiàn)域醇,也沒有找到當(dāng)時的堆棧信息台谊,所以不得不整個流程都走了一遍......
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蓉媳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子锅铅,更是在濱河造成了極大的恐慌酪呻,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盐须,死亡現(xiàn)場離奇詭異玩荠,居然都是意外死亡,警方通過查閱死者的電腦和手機贼邓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門阶冈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人塑径,你說我怎么就攤上這事女坑。” “怎么了统舀?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵匆骗,是天一觀的道長。 經(jīng)常有香客問我誉简,道長碉就,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任闷串,我火速辦了婚禮铝噩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘窿克。我一直安慰自己骏庸,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布年叮。 她就那樣靜靜地躺著具被,像睡著了一般。 火紅的嫁衣襯著肌膚如雪只损。 梳的紋絲不亂的頭發(fā)上一姿,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天,我揣著相機與錄音跃惫,去河邊找鬼叮叹。 笑死,一個胖子當(dāng)著我的面吹牛爆存,可吹牛的內(nèi)容都是我干的蛉顽。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼先较,長吁一口氣:“原來是場噩夢啊……” “哼携冤!你這毒婦竟也來了悼粮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤曾棕,失蹤者是張志新(化名)和其女友劉穎扣猫,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翘地,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡申尤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了衙耕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瀑凝。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖臭杰,靈堂內(nèi)的尸體忽然破棺而出粤咪,到底是詐尸還是另有隱情,我是刑警寧澤渴杆,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布寥枝,位于F島的核電站,受9級特大地震影響磁奖,放射性物質(zhì)發(fā)生泄漏囊拜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一比搭、第九天 我趴在偏房一處隱蔽的房頂上張望冠跷。 院中可真熱鬧,春花似錦身诺、人聲如沸蜜托。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽橄务。三九已至,卻和暖如春穴亏,著一層夾襖步出監(jiān)牢的瞬間蜂挪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工嗓化, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留棠涮,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓刺覆,卻偏偏與公主長得像严肪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

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

  • 從三月份找實習(xí)到現(xiàn)在诬垂,面了一些公司,掛了不少伦仍,但最終還是拿到小米结窘、百度、阿里充蓝、京東隧枫、新浪、CVTE谓苟、樂視家的研發(fā)崗...
    時芥藍閱讀 42,218評論 11 349
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法官脓,類相關(guān)的語法,內(nèi)部類的語法涝焙,繼承相關(guān)的語法卑笨,異常的語法,線程的語...
    子非魚_t_閱讀 31,602評論 18 399
  • 〔ps.可能正文和標題完全沒有關(guān)系,因為實在想不到題目了隧哮,文章的腦洞開得有點大吧桶良,朋友都說看不懂,希望能有人看懂沮翔,...
    哈拉米閱讀 318評論 0 1
  • 琴音分享 2016.12.31 ——關(guān)于心理營養(yǎng)“生命的至重” 女兒今天問了我一個這樣的問題:爸爸瑰步,如果我和媽媽同...
    王燕惠閱讀 2,814評論 0 1
  • 這幾天和閨密來上海看F1灾螃,今天順便約了高中同學(xué)吃飯题翻。 先說一下,約的是個男生,我高中的同桌嵌赠,學(xué)霸級別塑荒,高中時班主任...
    2ec19d3a3f77閱讀 284評論 0 0