项目实战十-文件上传、打包部署

文件上传

  1. 考试/科2科3下的添加页面,需要上传图片
  2. 上传原理:
    1. 后台拿到前端上传的图片流数据
    2. 将图片流数据保存到服务器本地磁盘下绝对路径F:/jk/upload/img/xxx.jpg,然后返回给客户端图片的相对路径:upload/img/xxx.jpg
    3. 后台需要做一个相对、绝对路径的映射,以便于解析客户端传递过来的相对路径:http://localhost:63342/upload/img/xxx.jpg

添加、编辑功能

  1. 添加pom.xml依赖
    1. 因为上传文件用到io操作,因此添加commons-io依赖
     <!-- IO -->
     <dependency>
         <groupId>commons-io</groupId>
         <artifactId>commons-io</artifactId>
         <version>${commons-io.version}</version>
     </dependency>
    
  2. 配置文件添加配置
    1. 设置文件上传的磁盘路径application-dev/pro.yml

       jk:
       #设置文件的上传路径
         upload:
           base-path: F:/jk/
           upload-path: upload/
           image-path: img/
           video-path: video/
      
    2. 设置整个项目的文件传输最大值、本地磁盘的映射路径,application.yml

       servlet:
         multipart:
           max-file-size: 20MB
           max-request-size: 20MB
       # 做一个映射
       resources:
         # 告知静态资源的路径在哪
         static-locations:
           - file:///${jk.upload.base-path}
      
    3. JkProperties同步添加对应的类、属性

      1. 提供一个当前类的工厂方法供外界(非容器中的对象)能够拿到
        1. 实现ApplicationContextAware接口,监听当前实例被创建加载到容器中的时刻
        2. setApplicationContext:设置当前类实例
      2. 添加upload类,以及属性
       //读取application-dev、pro下的yml对应的jk属性
       @ConfigurationProperties("jk")
       //将当前对象放入到容器
       @Component
       @Data
       public class JkProperties implements ApplicationContextAware {
           private Cfg cfg;
           //Upload类
           private Upload upload;
           //目的是让外界(非容器中的对象)能够拿到
           private static JkProperties properties;
           public static JkProperties get() {
               return properties;
           }
           //ApplicationContextAware的实现,专门用于监听当前类创建对象后放入到容器后调用
           @Override
           public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
               properties = this;
           }
              
           @Data
           public static class Cfg {
               private String[] corsOrigins;
           }
              
           @Data
           public static class Upload {
               private String basePath;
               private String uploadPath;
               private String imagePath;
               private String videoPath;
               //图片的相对路径
               public String getImageRelativePath() {
                   return uploadPath + imagePath;
               }
               //视频相对路径
               public String getVideoRelativePath() {
                   return uploadPath + videoPath;
               }
           }
       }
      
  3. 封装一个Uploads工具类,专门用于处理文件上传、下载、删除

     public class Uploads {
         //通过静态方法,拿到配置文件yml中的配置
         private static final JkProperties.Upload UPLOAD;
         static {
             UPLOAD = JkProperties.get().getUpload();
         }
        
         /**
          * 上传图片
          * @param multipartFile 图片数据
          */
         public static String uploadImage(MultipartFile multipartFile) throws Exception {
             // 目录(相对)
             String dirPath = UPLOAD.getImageRelativePath();
             // 返回图片对象
             return uploadFile(multipartFile, dirPath);
         }
        
         /**
          * 上传文件
          * @param multipartFile 文件数据
          * @param dir 文件存放到哪一个相对路径(比如如果是图片,相对路径是upload/img/)
          */
         private static String uploadFile(MultipartFile multipartFile, String dir) throws Exception {
             if (multipartFile == null || multipartFile.getSize() == 0) return null;
             // 文件扩展名
             String extension = FilenameUtils.getExtension(multipartFile.getOriginalFilename());
             // 文件名
             String filename = UUID.randomUUID() + "." + extension;
             // 文件路径(相对,upload/img/xxx.jpg)
             String filepath = dir + filename;
             // 文件路径(绝对,F:/jk/upload/img/xxx.jpg)
             String fullFilepath = UPLOAD.getBasePath() + filepath;
             File file = new File(fullFilepath);
             // 如果文件不存在,那就会创建文件夹
             FileUtils.forceMkdirParent(file);
             // 剪切,将客户端传递过来的文件剪切到磁盘目录
             multipartFile.transferTo(file);
             // 返回图片对象,相对路径
             return filepath;
         }
        
         public static void deleteFile(String relativeFilepath) throws Exception {
             if (Strings.isEmpty(relativeFilepath)) return;
             String fullFilepath = UPLOAD.getBasePath() + relativeFilepath;
             File file = new File(fullFilepath);
             // 如果是目录,就不删除
             if (file.isDirectory()) return;
        
             // 强制删除文件
             FileUtils.forceDelete(file);
         }
     }
    
  4. 接收前端上传reqvo改造ExamPlaceCourseReqVo,新增2个字段

     //封面图片数据:前端传递给后台的文件数据流
     private MultipartFile coverFile;
     //旧封面的路径,前端传递给后台的图片相对路径,编辑时上传
     private String cover;
    
  5. service层改造

     //ExamPlaceCourseService
     boolean saveOrUpdate(ExamPlaceCourseReqVo reqVo);
        
     //ExamPlaceCourseServiceImpl
     //考虑编辑、添加两种情况
     @Override
     public boolean saveOrUpdate(ExamPlaceCourseReqVo reqVo) {
         try {
             ExamPlaceCourse po = MapStructs.INSTANCE.reqVo2po(reqVo);
    
             // 上传图片
             String filepath = Uploads.uploadImage(reqVo.getCoverFile());
    
             // 有新的图片上传成功,如果用户是编辑但是并没有更新图片,就不需要设置新的图片封面路径
             if (filepath != null) {
                 // 设置新的封面,编辑重新上传图片,或者新添加图片
                 po.setCover(filepath);
             }
    
             // 保存po 到数据库中
             boolean ret = saveOrUpdate(po);
             if (ret && filepath != null) {
                 // 删除旧封面,编辑情形
                 Uploads.deleteFile(reqVo.getCover());
             }
             return ret;
         } catch (Exception e) {
             return JsonVos.raise(CodeMsg.UPLOAD_IMG_ERROR);
         }
     }
    
  6. ExamPlaceCourseController改造save方法

     @Override
     @RequiresPermissions(value = {
             Constants.Permisson.EXAM_PLACE_COURSE_ADD,
             Constants.Permisson.EXAM_PLACE_COURSE_UPDATE
     }, logical = Logical.AND)
     public JsonVo save(@Valid ExamPlaceCourseReqVo examPlaceCourseReqVo) {
         if (service.saveOrUpdate(examPlaceCourseReqVo)) {
             return JsonVos.ok(CodeMsg.SAVE_OK);
         } else {
             return JsonVos.raise(CodeMsg.SAVE_ERROR);
         }
     }
    
  7. 设置返回给前端封面路径
    1. ExamPlaceCourseVo新增字段

       /**
       * 封面
       */
       private String cover;
      
    2. ExamPlaceCourseMapper.xml中添加cover的字段查询
    3. 以上返回前端时就会从数据库查询到封面的路径url返回
  8. 注意: ShiroCfg中一定要放开静态资源访问

     //shiroFilterFactoryBean方法中
     // 上传的内容(/upload/**)
     urlMap.put("/" + properties.getUpload().getUploadPath() + "**", "anon");
    

删除功能

  1. 删除分为单个、批量删除,ExamPlaceCourseController添加remove方法

     /**
     * 删除功能,同时要删除列表数据对应的图片、视频文件
      * 同时要防止impl中的回滚事务,因此serviceimpl中只提供单个删除的方法removeById,不提供批量删除的方法
      * */
     @Override
     @RequiresPermissions(Constants.Permisson.EXAM_PLACE_COURSE_REMOVE)
     public JsonVo remove(String id) {
         List<String> idStrs = Arrays.asList(id.split(","));
         if (CollectionUtils.isEmpty(idStrs)) {
             return JsonVos.raise(CodeMsg.REMOVE_ERROR);
         }
    
         boolean ret = true;
         for (String idStr : idStrs) {
             if (!service.removeById(idStr)) {
                 ret = false;
             }
         }
    
         return ret ? JsonVos.ok(CodeMsg.REMOVE_OK) : JsonVos.raise(CodeMsg.REMOVE_ERROR);
     }
    
    1. 注意:这里的批量删除并没有用在service的impl中使用Mybatis-Plus的removeByIds方法,
    2. 因为service有事务回滚,一旦一个删除失败,其他都会回滚,因此servcie仅仅提供单个数据删除即可
    3. 删除每条数据时还要删除这条数据对应的图片、视频资源文件
  2. service层

     //重写MP的removeById方法,自定义实现
     @Override
     public boolean removeById(Serializable id) {
         ExamPlaceCourse course = getById(id);
         try {
             if (super.removeById(id)) {
                 Uploads.deleteFile(course.getCover());
             }
             return true;
         } catch (Exception e) {
             return false;
         }
     }
    

企业中的文件上传

  1. 文件数据跟随表单数据一起提交
  2. 文件数据先单独提交,先从文件服务器(oss服务器)获取一个uri,文件的uri跟随表单数据一起提交

打包部署

  1. 常见的包有2种形式
    1. jar包:内部已经包含tomcat,直接使用java-jar bao.jar,就可以运行, 专门针对springboot项目
    2. war包:内部不包含tomcat,需要将包放到tomcat的webapps文件目录下,然后运行tomact/bin/startup.bat方法即可,注意访问api时路径要加上contentpath
  2. 不管是哪种包,都要添加以下依赖

     <build>
         //打包的包名
         <finalName>logic_delete</finalName>
         <plugins>
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
         </plugins>
     </build>
    

jar包

  1. 当采取jar包方式部署时,packing设置为jar(默认就是jar),可以不写

     <packaging>jar</packaging>
    
  2. 如何局部覆盖jar包中的配置内容(根据之前讲过的配置文件那一节),配置文件的优先级

    1. 在优先级更高的位置存放新的配置文件
    2. java -jar --spring.config.location指定

war包(springboot项目)

  1. packing设置为war

     <packaging>war</packaging>
    
  2. 添加servlet依赖,排除SpringBoot内置的tomcat

     <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>javax.servlet-api</artifactId>
         <scope>provided</scope>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
         <exclusions>
             <exclusion>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-starter-tomcat</artifactId>
             </exclusion>
         </exclusions>
     </dependency>
    
  3. 修改启动类

    1. 继承 extends SpringBootServletInitializer 类
    2. 重写configure方法

       @SpringBootApplication
       @MapperScan("com.zh.mapper")
       public class LogicDeleteApplication extends SpringBootServletInitializer {
           public static void main(String[] args) {
               SpringApplication.run(LogicDeleteApplication.class, args);
           }
              
           @Override
           protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
               return builder.sources(LogicDeleteApplication.class);
           }
       }
      

打包

  1. 打成 war 包 (两种方式),然后放置于 Tomcat 的 webapps 目录下加载运行就行了
  2. war包部署的默认访问方式和jar 包的有点不同,war包访问 URL 需要加上项目名
  3. 打包方式一:在控制台输入打包命令:mvn clean package
  4. 打包方式二:
    1. Maven工具可视化界面打包 (需要spring-boot-maven-plugin依赖)
    2. 在项目添加spring-boot-maven-plugin依赖插件
    3. 在IDEA右侧点击Maven图标,找到生命周期,然后点击 clean、package即可
  5. war包在 target 包里面,将 war 包放入Tomcat 的 webapps 目录下 即可
  6. 这是将springboot自带的tomcat给剔除掉,然后用服务器自己安装的tomcat来部署项目,然后将当前的war包上传到服务器tomca的webapps文件下,然后启动tomcat
Table of Contents