简介
本文主要以文件上传为demo,介绍了一些 springboot web
开发的入门的技术栈。
对应刚接触 springboot
的可以参考下。
主要包括文件md5比对、生成图片缩略图、数据库迁移、文件记录持久化、请求全局异常处理等功能。
准备工作
- 在
idea
中创建项目,java8
,springboot 2
maven
所需依赖
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.bimcc</groupId> <artifactId>iot</artifactId> <version>0.0.1-SNAPSHOT</version> <name>iot</name> <description>iot</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.6.13</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--数据库迁移 --> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> <version>5.2.4</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!--请求验证--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>2.7.8</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>20.0</version> </dependency> <!--工具包--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.4.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <configuration> <mainClass>com.bimcc.iot.IotApplication</mainClass> <skip>true</skip> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
- 创建项目目录
目录意义如下:
- 修改
application.yml
配置:
--- # 开发环境的配置 server: port: 9090 spring: controller: api-prefix: /api flyway: enabled: true #开启数据迁移 table: flyway_schema_history #用于存储迁移历史记录的表名,默认为flyway_schema_history baseline-on-migrate: true #当迁移数据库存在但没有元数据的表时,自动执行基准迁移,新建flyway_schema_history表 locations: classpath:db/migration #数据库迁移脚本的位置,默认为classpath:db/migration,classpath 羡慕resources目录 clean-disabled: true #用于控制是否禁用 Flyway 的 clean 操作。 datasource: username: root password: root url: jdbc:mysql://127.0.0.1:3306/java_iot?serverTimezone=GMT%2b8 config: activate: on-profile: dev #开发环境 servlet: multipart: enabled: true # 允许文件上传 max-file-size: 20971520 # 单文件最大限制 20M max-request-size: 52428800 # 单次请求最大限制 50M file: upload: path: E:\project-java\java-upload # 文件上传保存服务器绝对路径 suffix: jpg,jpeg,png,bmp,xls,xlsx,pdf # 文件上传保存路径 is-thumb: true # 是否开启缩略图 true false proportion: 5 # 缩略图缩放比例 path-pattern: uploads # 访问虚拟目录 log: level: INFO # INFO DEBUG ERROR --- # 当前启用的配置 spring: application: name: iot # 应用平台 profiles: active: dev # 当前环境
- 创建数据表迁移文件
写入以下内容:
CREATE TABLE sys_file ( id INT AUTO_INCREMENT COMMENT 'id', file_name VARCHAR(255) NOT NULL COMMENT '文件名称', ip VARCHAR(255) COMMENT '上传ip', file_path VARCHAR(255) NOT NULL COMMENT '文件路径', thumb_path VARCHAR(255) COMMENT '缩略图文件路径', file_size INT COMMENT '字节大小', file_type VARCHAR(255) COMMENT '文件类型', file_ext CHAR(36) COMMENT '文件后缀', file_md5 VARCHAR(255) COMMENT '文件md5', created_at DATETIME COMMENT '创建时间', updated_at DATETIME COMMENT '修改时间', deleted_at DATETIME COMMENT '删除时间', PRIMARY KEY (id) ) ENGINE = InnoDB DEFAULT CHARSET = UTF8MB4;
创建上传实体类
- 在
dto目录
创建BaseEntity
及FileEntity
实体类
BaseEntity
写入以下内容:
// 省略 package import public class BaseEntity implements Serializable { value = "id",type = IdType.AUTO) ( private Integer id; pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") //格式化时间,空值不会格式化 ( value = "created_at",fill = FieldFill.INSERT) ( "created_at") //json格式化显示字段 ( private Date createdAt; pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") ( value = "updated_at",fill = FieldFill.INSERT_UPDATE) ( "updated_at") ( private Date updatedAt; pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") ( "deleted_at") ( value = "deleted_at") ( value = "null",delval = "now()") ( private Date deletedAt; }
FileEntity
写入以下内容:
// 省略 package import "sys_file") (public class FileEntity extends BaseEntity { value = "file_name") ( private String fileName; value = "ip") ( private String ip; value = "file_path") ( private String filePath; value = "thumb_path") ( private String thumbPath; value = "file_size") ( private Long fileSize; value = "file_type") ( private String fileType; value = "file_ext") ( private String fileExt; value = "file_md5") ( private String fileMd5; }
- 创建
mapper
, 在mapper
目录创建FileMapper
接口类
// 省略 package import public interface FileMapper extends BaseMapper<FileEntity> { FileEntity queryByMd5(String md5); }
- 创建
mapper xml
文件。在resources
目录下面创建mapper
目录,然后再创建FileMapper.xml
。并写入以下内容
<mapper namespace="com.bimcc.iot.mapper.FileMapper"> <!-- 询的结果集字段和实体类的user属性名不一致,自定义查询结果集的映射规则 --> <resultMap id="queryFile" type="com.bimcc.iot.dto.FileEntity"> <id property="id" column="id"/> <result property="fileName" column="file_name"/> <result property="ip" column="ip"/> <result property="filePath" column="file_path"/> <result property="fileSize" column="file_size"/> <result property="fileType" column="file_type"/> <result property="fileExt" column="file_ext"/> <result property="fileMd5" column="file_md5"/> <result property="createdAt" column="created_at"/> <result property="updatedAt" column="updated_at"/> <result property="deletedAt" column="deleted_at"/> </resultMap> <select id="queryByMd5" resultMap="queryFile"> select * from sys_file where file_md5 = #{md5} and deleted_at is null </select> </mapper>
全局异常处理
- 在
exceptin
目录里面创建ServiceException
类,编写如下代码:
// 省略 package import //专用于处理业务层的异常基类 public class ServiceException extends RuntimeException{ public ServiceException() { super(); } public ServiceException(String message) { super(message); } public ServiceException(String message, Throwable cause) { super(message, cause); } public ServiceException(Throwable cause) { super(cause); } protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
- 在
controller
目录里面创建BaseController
基类,后续controller
继承他
// 省略 package import public class BaseController { public static final int OK = 200; /** * 全局手动抛出异常处理 * 1.当出现了value内的异常之一,就会将下方的方法作为新的控制器方法进行执行 * 因此该方法的返回值也同时是返回给前端的页面 * 2.此外还自动将异常对象传递到此方法的参数列表中,这里使用Throwable e来接收 **/ ServiceException.class) //统一处理抛出的异常 ( public ResJson<Void> handleException(Throwable e){ ResJson<Void> result = new ResJson<>(e); result.setCode(5000); //数据库或服务器有问题 return result; } }
注册配置
在 config
目录创建 GlobalControllerPathPrefixConfig
类,写入以下内容:
// 省略 package import //群集统一配置接口前缀 public class GlobalControllerPathPrefixConfig implements WebMvcConfigurer { "${spring.controller.api-prefix}") ( private String pathPrefix; "${file.upload.path-pattern}") ( private String pathPattern; "${file.upload.path}") ( private String fileUploadPath; //全局接口注册 api前缀 public void configurePathMatch(PathMatchConfigurer configurer) { configurer.addPathPrefix(pathPrefix, c -> c.isAnnotationPresent(RestController.class)); } //静态资源图片上传,虚拟路径返回 public void addResourceHandlers(ResourceHandlerRegistry registry) { //将匹配上/files/**虚拟路径的url映射到文件上传到服务器的目录,获取静态资源 registry.addResourceHandler("/" +pathPattern + "/**").addResourceLocations("file:" + fileUploadPath+File.separator); WebMvcConfigurer.super.addResourceHandlers(registry); } }
创建工具类
- 创建一个全局返回类。在
utils
里面创建一个ResJson
类型
// 省略 import public class ResJson<E> implements Serializable { /** * 状态码 */ private Integer code; /** * 提示信息 */ private String message; /** * 返回数据 */ private E data; public ResJson(Integer code) { this.code = code; } public ResJson(Integer code,String message) { this.code = code; this.message = message; } public ResJson(Throwable e) { this.message = e.getMessage(); } public ResJson(Integer code,String message,E data) { this.code = code; this.data = data; this.message = message; } }
- 创建一个
md5
加密类。在utils
里面创建一个PasswordEncryptedUtils
类型
// 省略 package import public class PasswordEncryptedUtils { public static String getPasswordByMD5(String pwd,String salt){ for (int i = 0; i < 3 ; i++) { //md5加密算法的调用 pwd = DigestUtils.md5DigestAsHex((salt + pwd + salt).getBytes()).toUpperCase(); } //返回经过加密的结果 return pwd; } }
创建上传服务
- 在
server
目录里面创建FileServer
接口类
// 省略 package import public interface FileServer { FileEntity upload(MultipartFile file); String createThumb(String fileDir,String filePath,String fileName,String suffix); }
- 在
server目录
里面创建impl目
录并在里面创建FileServerImpl
类实现上面的接口功能
// 省略 package import public class FileServerImpl implements FileServer { public static final int maxWidth = 100; //拦截的url,虚拟路径 public String pathPattern = "uploads"; //文件磁盘路径 "${file.upload.path}") ( private String fileUploadPath; value = "${file.upload.suffix:jpg,jpeg,png,bmp,xls,xlsx,pdf}") ( private String fileUploadSuffix; value = "${file.upload.is-thumb}") ( private Boolean isThumb; value = "${file.upload.proportion}") ( private Integer proportion; HttpServletRequest request; FileMapper fileMapper; //文件上传 public FileEntity upload(MultipartFile file) { FileEntity fileRes = new FileEntity(); if (file.isEmpty()) { // log.error("the file to be uploaded is empty"); return fileRes; } List<String> suffixList = Lists.newArrayList(fileUploadSuffix.split(",")); try { //校验文件后缀 String originalFilename = file.getOriginalFilename(); //获取文件类型 String type = FileUtil.extName(originalFilename); //文件后缀 String suffix = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); if (!suffixList.contains(suffix)) { //log.error("unsupported file format"); return fileRes; } //获取文件md5 String md5 = SecureUtil.md5(file.getInputStream()); // 从数据库查询是否存在相同的记录 FileEntity dbFiles = fileMapper.queryByMd5(md5); if (dbFiles != null) { // 文件已存在 return dbFiles; } String year = new SimpleDateFormat("yyyy").format(new Date()); String month = new SimpleDateFormat("MM").format(new Date()); String day = new SimpleDateFormat("dd").format(new Date()); String fileDir = fileUploadPath; String filePath = File.separator + year + File.separator + month + File.separator + day + File.separator; //首次需生成目录 File folder = new File(fileDir + filePath); if (!folder.exists()) { folder.mkdirs(); } AtomicInteger counter = new AtomicInteger(0); String uniqueString = String.valueOf(Instant.now().toEpochMilli()); String fileName = uniqueString + "." + suffix; String absolutePath = fileDir + filePath + fileName; file.transferTo(new File(absolutePath)); //网页路径 String dataFilePath = pathPattern + "/" + year + "/" + "/" + month + "/" + day + "/" + fileName; fileRes.setFilePath(dataFilePath); fileRes.setFileName(fileName); fileRes.setIp(request.getRemoteAddr()); fileRes.setFileSize(file.getSize() / 1024); fileRes.setFileType(type); fileRes.setFileExt(suffix); fileRes.setFileMd5(md5); //判断是否生成缩率图 if (isThumb) { createThumb(fileDir, filePath, uniqueString, suffix); String dataFileThumbPath = pathPattern + "/" + year + "/" + "/" + month + "/" + day + "/" + uniqueString + "_thumb." + suffix; fileRes.setThumbPath(dataFileThumbPath); } fileMapper.insert(fileRes); } catch (IOException e) { e.printStackTrace(); throw new ServiceException(e.getMessage()); } return fileRes; } //生成缩率图 public String createThumb(String fileDir, String filePath, String fileName, String suffix) { String localPath = fileDir + filePath + fileName + "." + suffix; String thumbPath = fileDir + filePath + fileName + "_thumb." + suffix; //判断缩略图是否存在 Path path = Paths.get(thumbPath); if (Files.exists(path)) { return filePath + fileName + "_thumb." + suffix; } File originalFile = new File(localPath); try { BufferedImage originalImage = ImageIO.read(originalFile); int imageWidth = originalImage.getWidth(); int imageHeight = originalImage.getHeight(); double thumbWidth = 0; double thumbHeight = 0; if (imageWidth > maxWidth || imageHeight > maxWidth) { thumbWidth = (double) imageWidth / (double) proportion; thumbHeight = (double) imageHeight / (double) proportion; } if (thumbHeight > 0) { // 创建缩略图 BufferedImage thumbnail = new BufferedImage((int) thumbWidth, (int) thumbHeight, BufferedImage.TYPE_INT_RGB); Graphics graphics = thumbnail.createGraphics(); graphics.drawImage(originalImage.getScaledInstance((int) thumbWidth, (int) thumbHeight, Image.SCALE_SMOOTH), 0, 0, null); graphics.dispose(); // 输出到文件 ImageIO.write(thumbnail, suffix, new File(thumbPath)); return filePath + fileName + "_thumb." + suffix; } } catch (IOException e) { e.printStackTrace(); throw new ServiceException(e.getMessage()); } return ""; } }
上传
- 创建一个上传控制器
UploadController
,写入以下内容:
// 省略 package import public class UploadController extends BaseController { FileServer fileServer; "/upload") ( public ResJson<FileEntity> upload( MultipartFile file){ FileEntity fileRes= fileServer.upload(file); return new ResJson<>(OK,"上传成功!",fileRes); } }
- 测试
通过 postman
接口测试,调用上面的上传接口
查看application.yml
里面配置的上传目录,是否有文件
通过网络路径访问图片
总结
本文适合 springboot
入门的初学者。
以文件上传为demo,衍生出了一些常用功能,包含:文件上传,入库,请求。异常处理,api前缀,数据迁移,生成缩略图,等功能。
希望能对初学者有一个参考的作用。