Docker下Java文件上传服务三部曲之二:服务端开发

Stella981
• 阅读 652

本章是《Docker下Java文件上传服务三部曲》的第二篇,上一章《Docker下Java文件上传服务三部曲之一:准备环境》我们把客户端准备好了,Tomcat容器也部署好了,今天就来开发和部署文件服务的后台应用吧;

原文地址:http://blog.csdn.net/boling\_cavalry/article/details/79367520

三部曲所有文章链接

  1. 《Docker下Java文件上传服务三部曲之一:准备环境》
  2. 《Docker下Java文件上传服务三部曲之二:服务端开发》
  3. 《Docker下Java文件上传服务三部曲之三:wireshark抓包分析》

本章实战内容概要

本章要创建三个web应用,都是文件上传的服务端,分别部署在ubuntu电脑上的三个Docker容器中,接收来自客户端的上传文件的请求,结构如下:
Docker下Java文件上传服务三部曲之二:服务端开发

三个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镜像

springboot工程是可以直接构建成docker镜像的,在docker下以此镜像创建容器,该springboot工程就直接运行起来了,如果您想了解具体构建的细节,请访问以下几篇文章:

  1. 《maven构建docker镜像三部曲之一:准备环境》
  2. 《maven构建docker镜像三部曲之二:编码和构建镜像》
  3. 《maven构建docker镜像三部曲之三:推送到远程仓库(内网和阿里云)》

三个web应用的源码下载

您可以在GitHub下载本章三个web应用的源码,地址和链接信息如下表所示:

名称

链接

备注

项目主页

https://github.com/zq2599/blog\_demos

该项目在GitHub上的主页

git仓库地址(https)

https://github.com/zq2599/blog\_demos.git

该项目源码的仓库地址,https协议

git仓库地址(ssh)

git@github.com:zq2599/blog_demos.git

该项目源码的仓库地址,ssh协议

这个git项目中有多个目录,本次所需的资源放在springmvcfileserver、fileserverdemo、springbootfileserver这三个目录下,如下图红框所示:

Docker下Java文件上传服务三部曲之二:服务端开发

SpirngMVC框架如何处理上传的文件?

SpirngMVC对POST请求中的二进制文件的处理,是依赖apache的commons-fileupload库来完成的,如果您想了解更多细节,请参考文章《SpringMVC源码分析:POST请求中的文件处理》

创建应用springmvcfileserver

  1. 创建一个maven工程springmvcfileserver,pom.xml内容如下:

    4.0.0 com.bolingcavalry springmvcfileserver war 1.0-SNAPSHOT springmvcfileserver Maven Webapp http://maven.apache.org

    4.0.2.RELEASE org.springframework spring-core ${spring.version} org.springframework spring-web ${spring.version} org.springframework spring-webmvc ${spring.version} org.springframework spring-aop ${spring.version} org.springframework spring-context-support ${spring.version} javax javaee-api 7.0
    <!-- 映入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>
    
    ${project.artifactId}
    <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>
    

注意上述配置中的http://192.168.119.155:8088/manager/text这个地址,“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>
  1. spring-mvc.xml中要配置multipartResolver:

  2. 在spring-extends.xml中配置自动扫描的规则:

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

  3. 上传服务的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库解析出来的,这里直接用就好了;
7. 在pom.xml文件的目录执行以下命令,即可编译构建war包,并部署到Tomcat容器上去:

mvn clean package -U -Dmaven.test.skip=true tomcat7:deploy
  1. 应用部署完毕后,可以通过上一章开发的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
  1. 应用部署完毕后,可以通过上一章开发的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镜像:

            <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抓包分析》

欢迎关注我的公众号:程序员欣宸

Docker下Java文件上传服务三部曲之二:服务端开发

本文同步分享在 博客“程序员欣宸”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Stella981 Stella981
3年前
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解2016年09月02日00:00:36 \牧野(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fme.csdn.net%2Fdcrmg) 阅读数:59593
Stella981 Stella981
3年前
Docker下dubbo开发三部曲之三:java开发
在前两章《Docker下dubbo开发,三部曲之一:极速体验》(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fblog.csdn.net%2Fboling_cavalry%2Farticle%2Fdetails%2F72303126)和《Docker下dubbo开发,三部曲之二:本地环
Wesley13 Wesley13
3年前
P2P技术揭秘.P2P网络技术原理与典型系统开发
Modular.Java(2009.06)\.Craig.Walls.文字版.pdf:http://www.t00y.com/file/59501950(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.t00y.com%2Ffile%2F59501950)\More.E
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Java服务总在半夜挂,背后的真相竟然是... | 京东云技术团队
最近有用户反馈测试环境Java服务总在凌晨00:00左右挂掉,用户反馈Java服务没有定时任务,也没有流量突增的情况,Jvm配置也合理,莫名其妙就挂了
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这