项目实战十-文件上传、打包部署
文件上传
- 考试/科2科3下的添加页面,需要上传图片
- 上传原理:
- 后台拿到前端上传的图片流数据
- 将图片流数据保存到服务器本地磁盘下绝对路径
F:/jk/upload/img/xxx.jpg
,然后返回给客户端图片的相对路径:upload/img/xxx.jpg
- 后台需要做一个相对、绝对路径的映射,以便于解析客户端传递过来的相对路径:
http://localhost:63342/upload/img/xxx.jpg
添加、编辑功能
- 添加pom.xml依赖
- 因为上传文件用到io操作,因此添加commons-io依赖
<!-- IO --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>${commons-io.version}</version> </dependency>
- 配置文件添加配置
-
设置文件上传的磁盘路径application-dev/pro.yml
jk: #设置文件的上传路径 upload: base-path: F:/jk/ upload-path: upload/ image-path: img/ video-path: video/
-
设置整个项目的文件传输最大值、本地磁盘的映射路径,application.yml
servlet: multipart: max-file-size: 20MB max-request-size: 20MB # 做一个映射 resources: # 告知静态资源的路径在哪 static-locations: - file:///${jk.upload.base-path}
-
JkProperties同步添加对应的类、属性
- 提供一个当前类的工厂方法供外界(非容器中的对象)能够拿到
- 实现ApplicationContextAware接口,监听当前实例被创建加载到容器中的时刻
- setApplicationContext:设置当前类实例
- 添加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; } } }
- 提供一个当前类的工厂方法供外界(非容器中的对象)能够拿到
-
-
封装一个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); } }
-
接收前端上传reqvo改造ExamPlaceCourseReqVo,新增2个字段
//封面图片数据:前端传递给后台的文件数据流 private MultipartFile coverFile; //旧封面的路径,前端传递给后台的图片相对路径,编辑时上传 private String cover;
-
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); } }
-
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); } }
- 设置返回给前端封面路径
-
ExamPlaceCourseVo新增字段
/** * 封面 */ private String cover;
- ExamPlaceCourseMapper.xml中添加cover的字段查询
- 以上返回前端时就会从数据库查询到封面的路径url返回
-
-
注意: ShiroCfg中一定要放开静态资源访问
//shiroFilterFactoryBean方法中 // 上传的内容(/upload/**) urlMap.put("/" + properties.getUpload().getUploadPath() + "**", "anon");
删除功能
-
删除分为单个、批量删除,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); }
- 注意:这里的批量删除并没有用在service的impl中使用Mybatis-Plus的removeByIds方法,
- 因为service有事务回滚,一旦一个删除失败,其他都会回滚,因此servcie仅仅提供单个数据删除即可
- 删除每条数据时还要删除这条数据对应的图片、视频资源文件
-
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; } }
企业中的文件上传
- 文件数据跟随表单数据一起提交
- 文件数据先单独提交,先从文件服务器(oss服务器)获取一个uri,文件的uri跟随表单数据一起提交
打包部署
- 常见的包有2种形式
- jar包:内部已经包含tomcat,直接使用java-jar bao.jar,就可以运行, 专门针对springboot项目
- war包:内部不包含tomcat,需要将包放到tomcat的webapps文件目录下,然后运行tomact/bin/startup.bat方法即可,注意访问api时路径要加上contentpath
-
不管是哪种包,都要添加以下依赖
<build> //打包的包名 <finalName>logic_delete</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
jar包
-
当采取jar包方式部署时,packing设置为jar(默认就是jar),可以不写
<packaging>jar</packaging>
-
如何局部覆盖jar包中的配置内容(根据之前讲过的配置文件那一节),配置文件的优先级
- 在优先级更高的位置存放新的配置文件
- 在
java -jar --spring.config.location
指定
war包(springboot项目)
-
packing设置为war
<packaging>war</packaging>
-
添加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>
-
修改启动类
- 继承 extends SpringBootServletInitializer 类
-
重写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); } }
打包
- 打成 war 包 (两种方式),然后放置于 Tomcat 的 webapps 目录下加载运行就行了
- war包部署的默认访问方式和jar 包的有点不同,war包访问 URL 需要加上项目名
- 打包方式一:在控制台输入打包命令:
mvn clean package
- 打包方式二:
- Maven工具可视化界面打包 (需要spring-boot-maven-plugin依赖)
- 在项目添加spring-boot-maven-plugin依赖插件
- 在IDEA右侧点击Maven图标,找到生命周期,然后点击 clean、package即可
- war包在 target 包里面,将 war 包放入Tomcat 的 webapps 目录下 即可
- 这是将springboot自带的tomcat给剔除掉,然后用服务器自己安装的tomcat来部署项目,然后将当前的war包上传到服务器tomca的webapps文件下,然后启动tomcat