1. 程式人生 > >Docker下Java檔案上傳服務三部曲之二:服務端開發

Docker下Java檔案上傳服務三部曲之二:服務端開發

本章是《Docker下Java檔案上傳服務三部曲》的第二篇,上一章《Docker下Java檔案上傳服務三部曲之一:準備環境》我們把客戶端準備好了,Tomcat容器也部署好了,今天就來開發和部署檔案服務的後臺應用吧;

本章實戰內容概要

本章要建立三個web應用,都是檔案上傳的服務端,分別部署在ubuntu電腦上的三個Docker容器中,接收來自客戶端的上傳檔案的請求,結構如下:
這裡寫圖片描述

三個web應用功能相同,都是檔案上傳的服務端,它們的差別用下表來說明:

應用名 框架 檔案服務技術方案 Docker上的部署方式
springmvcfileserver spring mvc springframework的CommonsMultipartResolver 構建war,部署到Tomcat容器上
fileserverdemo spring mvc apache的commons-fileupload庫 構建war,部署到Tomcat容器上
springbootfileserver springboot springframework的CommonsMultipartResolver 構建jar,做成Docker映象

如何將war包線上部署到Tomcat容器上

springmvcfileserver和fileserverdemo在pom.xml中都用到了tomcat7-maven-plugin外掛,可以將war包線上部署到Tomcat容器上,記得修改本地的maven配置檔案,將登入tomcat的使用者名稱和密碼加進去,配置的詳情請參照

《實戰docker,編寫Dockerfile定製tomcat映象,實現web應用線上部署》

如何將springboot工程構建成docker映象

三個web應用的原始碼下載

您可以在GitHub下載本章三個web應用的原始碼,地址和連結資訊如下表所示:

名稱 連結 備註
git倉庫地址(ssh) [email protected]:zq2599/blog_demos.git 該專案原始碼的倉庫地址,ssh協議


這個git專案中有多個目錄,本次所需的資源放在springmvcfileserver、fileserverdemo、springbootfileserver這三個目錄下,如下圖紅框所示:
這裡寫圖片描述

SpirngMVC框架如何處理上傳的檔案?

SpirngMVC對POST請求中的二進位制檔案的處理,是依賴apache的commons-fileupload庫來完成的,如果您想了解更多細節,請參考文章《SpringMVC原始碼分析:POST請求中的檔案處理》

建立應用springmvcfileserver

  1. 建立一個maven工程springmvcfileserver,pom.xml內容如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.bolingcavalry</groupId>
  <artifactId>springmvcfileserver</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>springmvcfileserver Maven Webapp</name>
  <url>http://maven.apache.org</url>
  <properties>
    <!-- spring版本號 -->
    <spring.version>4.0.2.RELEASE</spring.version>
  </properties>
  <dependencies>
    <!-- spring核心包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- 匯入java ee jar 包 -->
    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-api</artifactId>
      <version>7.0</version>
    </dependency>

    <!-- 映入JSON -->
    <dependency>
      <groupId>org.codehaus.jackson</groupId>
      <artifactId>jackson-mapper-asl</artifactId>
      <version>1.9.13</version>
    </dependency>
    <!-- 上傳元件包 -->
    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.3.1</version>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.6</version>
    </dependency>

    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>1.0.9</version>
    </dependency>

    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.0.9</version>
    </dependency>

  </dependencies>

  <build>
    <finalName>${project.artifactId}</finalName>

    <resources>
      <resource>
        <directory>src/main/java</directory>
        <includes>
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <!-- 是否替換資源中的屬性-->
        <filtering>false</filtering>
      </resource>
      <resource>
        <directory>src/main/resources</directory>
      </resource>
    </resources>
    <plugins>
      <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
        <configuration>
          <url>http://192.168.119.155:8088/manager/text</url>
          <server>tomcat7</server>
          <path>/${project.artifactId}</path>
          <update>true</update>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

注意上述配置中的這個地址,“192.168.119.155”是我的ubuntu電腦的IP地址,8088是Tomcat容器啟動用對映到ubuntu電腦的埠號,請您按照自己電腦的實際情況修改;

2. 在web.xml中新增springmvc相關的配置:

<servlet>
    <servlet-name>SpringMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>
    <servlet-mapping>
        <servlet-name>SpringMVC</servlet-name>
        <!-- 此處可以可以配置成*.do,對應struts的字尾習慣 -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>


3. spring-mvc.xml中要配置multipartResolver:

<bean id="multipartResolver"
        class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="defaultEncoding" value="utf-8" />
    <property name="maxUploadSize" value="10485760000" />
    <property name="maxInMemorySize" value="40960" />
</bean>


4. 在spring-extends.xml中配置自動掃描的規則:

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


5. 上傳服務的controller是UploadController.java,響應請求的upload方法如下:

@RequestMapping(value="/upload",method= RequestMethod.POST)
    public void upload(HttpServletRequest request,
                       HttpServletResponse response,
                         @RequestParam("comment") String comment,
                         @RequestParam("file") MultipartFile file) throws Exception {

        logger.info("start upload, comment [{}]", comment);

        if(null==file || file.isEmpty()){
            logger.error("file item is empty!");
            responseAndClose(response, "檔案資料為空");
            return;
        }

        //上傳檔案路徑
        String savePath = request.getServletContext().getRealPath("/WEB-INF/upload");

        //上傳檔名
        String fileName = file.getOriginalFilename();

        logger.info("base save path [{}], original file name [{}]", savePath, fileName);

        //得到檔案儲存的名稱
        fileName = mkFileName(fileName);

        //得到檔案儲存的路徑
        String savePathStr = mkFilePath(savePath, fileName);

        logger.info("real save path [{}], real file name [{}]", savePathStr, fileName);

        File filepath = new File(savePathStr, fileName);

        //確保路徑存在
        if(!filepath.getParentFile().exists()){
            logger.info("real save path is not exists, create now");
            filepath.getParentFile().mkdirs();
        }

        String fullSavePath = savePathStr + File.separator + fileName;

        //存本地
        file.transferTo(new File(fullSavePath));

        logger.info("save file success [{}]", fullSavePath);

        responseAndClose(response, "Spring MVC環境下,上傳檔案成功");
    }

前面的分析中,我們已知道入參的MultipartFile物件是apache的commons-fileupload庫解析出來的,這裡直接用就好了;
6. 在pom.xml檔案的目錄執行以下命令,即可編譯構建war包,並部署到Tomcat容器上去:

mvn clean package -U -Dmaven.test.skip=true tomcat7:deploy


7. 應用部署完畢後,可以通過上一章開發的UploadFileClient類測試服務是否正常;

接下來我們建立第二個應用fileserverdemo;

建立應用fileserverdemo

應用fileserverdemo被設計成不使用spring mvc的檔案處理功能,而是自己寫程式碼呼叫apache的commons-fileupload庫來處理上傳的檔案,關鍵程式碼如下:
1. 在spring-mvc.xml中,不要配置multipartResolver,如果不配置multipartResolver的話,spring mvc是如何處理的呢?一起來看下DispatcherServlet.checkMultipart方法:

if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
            if (request instanceof MultipartHttpServletRequest) {
                logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
                        "this typically results from an additional MultipartFilter in web.xml");
            }
            else {
                return this.multipartResolver.resolveMultipart(request);
            }
        }
        // If not returned before: return original request.
        return request;

如果沒有multipartResolver,那麼該方法返回的就是傳入的request,該request最終會傳遞給業務Controller的響應方法中去;

2. 業務controller的響應方法如下:

@RequestMapping("/upload")
    public void upload(HttpServletRequest request, HttpServletResponse response) throws Exception{
        logger.info("start upload");
        //得到上傳檔案的儲存目錄,將上傳的檔案存放於WEB-INF目錄下,不允許外界直接訪問,保證上傳檔案的安全
        String savePath = request.getServletContext().getRealPath("/WEB-INF/upload");
        //上傳時生成的臨時檔案儲存目錄
        String tempPath = request.getServletContext().getRealPath("/WEB-INF/temp");

        logger.info("savePath [{}], tempPath [{}]", savePath, tempPath);

        File file = new File(tempPath);
        if(!file.exists()&&!file.isDirectory()){
            logger.info("臨時檔案目錄不存在logger.info,現在建立。");
            file.mkdir();
        }

        //訊息提示
        String message = "";
        try {
            //使用Apache檔案上傳元件處理檔案上傳步驟:
            //1、建立一個DiskFileItemFactory工廠
            DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
            //設定工廠的緩衝區的大小,當上傳的檔案大小超過緩衝區的大小時,就會生成一個臨時檔案存放到指定的臨時目錄當中。
            diskFileItemFactory.setSizeThreshold(1024*100);
            //設定上傳時生成的臨時檔案的儲存目錄
            diskFileItemFactory.setRepository(file);
            //2、建立一個檔案上傳解析器
            ServletFileUpload fileUpload = new ServletFileUpload(diskFileItemFactory);
            //解決上傳檔名的中文亂碼
            fileUpload.setHeaderEncoding("UTF-8");
            //監聽檔案上傳進度
            fileUpload.setProgressListener(new ProgressListener(){
                public void update(long pBytesRead, long pContentLength, int arg2) {
                    logger.debug("total [{}], now [{}]", pContentLength, pBytesRead);
                }
            });

            //3、判斷提交上來的資料是否是上傳表單的資料
            if(!fileUpload.isMultipartContent(request)){
                logger.error("this is not a file post");
                responseAndClose(response, "無效的請求引數,請提交檔案");
                //按照傳統方式獲取資料
                return;
            }

            //設定上傳單個檔案的大小的最大值,目前是設定為1024*1024*1024位元組,也就是1G
            fileUpload.setFileSizeMax(1024*1024*1024);
            //設定上傳檔案總量的最大值,最大值=同時上傳的多個檔案的大小的最大值的和,目前設定為10GB
            fileUpload.setSizeMax(10*1024*1024*1024);
            //4、使用ServletFileUpload解析器解析上傳資料,解析結果返回的是一個List<FileItem>集合,每一個FileItem對應一個Form表單的輸入項
            List<FileItem> list = fileUpload.parseRequest(request);

            logger.info("after parse request, file item size [{}]", list.size());

            for (FileItem item : list) {
                //如果fileitem中封裝的是普通輸入項的資料
                if(item.isFormField()){
                    String name = item.getFieldName();
                    //解決普通輸入項的資料的中文亂碼問題
                    String value = item.getString("UTF-8");
                    String value1 = new String(name.getBytes("iso8859-1"),"UTF-8");
                    logger.info("form field, name [{}], value [{}], name after convert [{}]", name, value, value1);
                }else{
                    //如果fileitem中封裝的是上傳檔案,得到上傳的檔名稱,
                    String fileName = item.getName();
                    logger.info("not a form field, file name [{}]", fileName);
                    if(fileName==null||fileName.trim().equals("")){
                        logger.error("invalid file name");
                        continue;
                    }
                    //注意:不同的瀏覽器提交的檔名是不一樣的,有些瀏覽器提交上來的檔名是帶有路徑的,如:  c:\a\b\1.txt,而有些只是單純的檔名,如:1.txt
                    //處理獲取到的上傳檔案的檔名的路徑部分,只保留檔名部分
                    fileName = fileName.substring(fileName.lastIndexOf(File.separator)+1);

                    //得到上傳檔案的副檔名
                    String fileExtName = fileName.substring(fileName.lastIndexOf(".")+1);

                    logger.info("ext name [{}], file name after cut [{}]", fileExtName, fileName);

                    if("zip".equals(fileExtName)||"rar".equals(fileExtName)||"tar".equals(fileExtName)||"jar".equals(fileExtName)){
                        logger.error("this type can not upload [{}]", fileExtName);
                        responseAndClose(response, "上傳檔案的型別不符合");
                        return;
                    }

                    //獲取item中的上傳檔案的輸入流
                    InputStream is = item.getInputStream();
                    //得到檔案儲存的名稱
                    fileName = mkFileName(fileName);
                    //得到檔案儲存的路徑
                    String savePathStr = mkFilePath(savePath, fileName);
                    System.out.println("儲存路徑為:"+savePathStr);
                    //建立一個檔案輸出流
                    FileOutputStream fos = new FileOutputStream(savePathStr+File.separator+fileName);
                    //建立一個緩衝區
                    byte buffer[] = new byte[1024];
                    //判斷輸入流中的資料是否已經讀完的標識
                    int length = 0;
                    //迴圈將輸入流讀入到緩衝區當中,(len=in.read(buffer))>0就表示in裡面還有資料
                    while((length = is.read(buffer))>0){
                        //使用FileOutputStream輸出流將緩衝區的資料寫入到指定的目錄(savePath + "\\" + filename)當中
                        fos.write(buffer, 0, length);
                    }
                    //關閉輸入流
                    is.close();
                    //關閉輸出流
                    fos.close();
                    //刪除處理檔案上傳時生成的臨時檔案
                    item.delete();
                    message = "檔案上傳成功";
                }
            }
        } catch (FileUploadBase.FileSizeLimitExceededException e) {
            logger.error("1. upload fail, ", e);
            responseAndClose(response, "單個檔案超出最大值!!!");
            return;
        }catch (FileUploadBase.SizeLimitExceededException e) {
            logger.error("2. upload fail, ", e);
            responseAndClose(response, "上傳檔案的總的大小超出限制的最大值!!!");
            return;
        }catch (FileUploadException e) {
            // TODO Auto-generated catch block
            logger.error("3. upload fail, ", e);
            message = "檔案上傳失敗:" + e.toString();
        }

        responseAndClose(response, message);
        logger.info("finish upload");
    }

上述程式碼有以下兩點需要注意:
a. 由於沒有multipartResolver 這個bean的配置,因此upload方法入參中沒有FileItem物件,request沒有做過檔案相關的解析和處理;
b. upload方法中建立了ServletFileUpload物件,並呼叫parseRequest方法解析出所有FileItem物件,然後按照業務需求做各種處理;
3. 其他的配置和springmvcfileserver工程一樣,可以參考GitHub上的原始碼;
4. 在pom.xml檔案的目錄執行以下命令,即可編譯構建war包,並部署到Tomcat容器上去:

mvn clean package -U -Dmaven.test.skip=true tomcat7:deploy


5. 應用部署完畢後,可以通過上一章開發的UploadFileClient類測試服務是否正常,記得修改POST_URL變數為“http://192.168.119.155:8088/fileserverdemo/upload”,把“192.168.119.155”換成你的ubuntu電腦的IP地址;

至此,兩個部署在Tomcat容器上的應用都完成了,接下來我們建立springbootfileserver應用,體驗springboot工程提供的檔案服務;

建立應用springbootfileserver

springbootfileserver應用對檔案的處理,本質上還是spring mvc的處理流程,和springmvcfileserver工程的差異在於:不需要我們配置multipartResolver這個bean,springboot啟動的時候已經在應用中預設建立了multipartResolver物件,以下是該工程的幾處關鍵程式碼:
1. pom.xml中新增一個外掛,用來將工程構建成docker映象:

<!--新增的docker maven外掛-->
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>0.4.12</version>
                <!--docker映象相關的配置資訊-->
                <configuration>
                    <!--映象名,這裡用工程名-->
                    <imageName>bolingcavalry/${project.artifactId}</imageName>
                    <!--TAG,這裡用工程版本號-->
                    <imageTags>
                        <imageTag>${project.version}</imageTag>
                    </imageTags>
                    <!--映象的FROM,使用java官方映象-->
                    <baseImage>java:8u111-jdk</baseImage>
                    <!--該映象的容器啟動後,直接執行spring boot工程-->
                    <entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
                    <!--構建映象的配置資訊-->
                    <resources>
                        <resource>
                            <targetPath>/</targetPath>
                            <directory>${project.build.directory}</directory>
                            <include>${project.build.finalName}.jar</include>
                        </resource>
                    </resources>
                </configuration>
            </plugin>


2. 業務controller的響應方法如下:

@RequestMapping(value="/upload",method= RequestMethod.POST)
    public void upload(HttpServletRequest request,
                       HttpServletResponse response,
                       @RequestParam("comment") String comment,
                       @RequestParam("file") MultipartFile file) throws Exception {

        logger.info("start upload, comment [{}]", comment);

        if(null==file || file.isEmpty()){
            logger.error("file item is empty!");
            responseAndClose(response, "檔案資料為空");
            return;
        }

        //上傳檔案路徑
        String savePath = request.getServletContext().getRealPath("/WEB-INF/upload");

        //上傳檔名
        String fileName = file.getOriginalFilename();

        logger.info("base save path [{}], original file name [{}]", savePath, fileName);

        //得到檔案儲存的名稱
        fileName = mkFileName(fileName);

        //得到檔案儲存的路徑
        String savePathStr = mkFilePath(savePath, fileName);

        logger.info("real save path [{}], real file name [{}]", savePathStr, fileName);

        File filepath = new File(savePathStr, fileName);

        //確保路徑存在
        if(!filepath.getParentFile().exists()){
            logger.info("real save path is not exists, create now");
            filepath.getParentFile().mkdirs();
        }

        String fullSavePath = savePathStr + File.separator + fileName;

        //存本地
        file.transferTo(new File(fullSavePath));

        logger.info("save file success [{}]", fullSavePath);

        responseAndClose(response, "SpringBoot環境下,上傳檔案成功");
    }

可見和springmvcfileserver工程的業務controller基本一致;
3. 在pom.xml所在目錄執行以下命令,即可將當前工程編譯構建,並製作成docker映象:

mvn clean package -DskipTests docker:build

注意:由於要製作docker映象,所以要求當前電腦同時安裝了maven和docker,推薦在ubuntu電腦上執行,因為做出來的映象稍後就會用到了;
4. 在ubuntu電腦上執行以下命令,就能將springbootfileserver工程在容器中執行起來:

docker run --name fileserver001 -p 8080:8080 -v /usr/local/work/fileupload/upload:/usr/Downloads -idt  bolingcavalry/springbootfileserver:0.0.1-SNAPSHOT

注意:此命令在上一章執行過,不同的是用的映象是剛剛構建的;

5. 用UploadFileClient類測試服務是否正常;

至此三個web應用就開發完成了,這三個應用在處理檔案服務的過程中,有的簡單易用,有的能把控更多細節用來滿足相對複雜的業務需求,希望能對您在實現業務需求時提供一些參考;

基本的開發和部署工作已經全部完成,下一章我們通過wireshark抓包,來看看HTTP的POST請求和響應的細節,看下一章請點選《Docker下Java檔案上傳服務三部曲之三:wireshar抓包分析》