简介
本文主要以文件上传为demo,介绍了一些 springboot web
开发的入门的技术栈。
对应刚接触 springboot
的可以参考下。
主要包括文件md5比对、生成图片缩略图、数据库迁移、文件记录持久化、请求全局异常处理等功能。
准备工作
- 在
idea
中创建项目,java8
,springboot 2
maven
所需依赖
<?xml version="1.0" encoding="UTF-8"?>
<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
@Data
public class BaseEntity implements Serializable {
@TableId(value = "id",type = IdType.AUTO)
private Integer id;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") //格式化时间,空值不会格式化
@TableField(value = "created_at",fill = FieldFill.INSERT)
@JsonProperty("created_at") //json格式化显示字段
private Date createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@TableField(value = "updated_at",fill = FieldFill.INSERT_UPDATE)
@JsonProperty("updated_at")
private Date updatedAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonProperty("deleted_at")
@TableField(value = "deleted_at")
@TableLogic(value = "null",delval = "now()")
private Date deletedAt;
}
FileEntity
写入以下内容:
// 省略 package import
@Data
@TableName("sys_file")
public class FileEntity extends BaseEntity {
@TableField(value = "file_name")
private String fileName;
@TableField(value = "ip")
private String ip;
@TableField(value = "file_path")
private String filePath;
@TableField(value = "thumb_path")
private String thumbPath;
@TableField(value = "file_size")
private Long fileSize;
@TableField(value = "file_type")
private String fileType;
@TableField(value = "file_ext")
private String fileExt;
@TableField(value = "file_md5")
private String fileMd5;
}
- 创建
mapper
, 在mapper
目录创建FileMapper
接口类
// 省略 package import
@Mapper
public interface FileMapper extends BaseMapper<FileEntity> {
FileEntity queryByMd5(String md5);
}
- 创建
mapper xml
文件。在resources
目录下面创建mapper
目录,然后再创建FileMapper.xml
。并写入以下内容
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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
@ControllerAdvice
public class BaseController {
public static final int OK = 200;
/**
* 全局手动抛出异常处理
* 1.当出现了value内的异常之一,就会将下方的方法作为新的控制器方法进行执行
* 因此该方法的返回值也同时是返回给前端的页面
* 2.此外还自动将异常对象传递到此方法的参数列表中,这里使用Throwable e来接收
**/
@ExceptionHandler(ServiceException.class) //统一处理抛出的异常
public ResJson<Void> handleException(Throwable e){
ResJson<Void> result = new ResJson<>(e);
result.setCode(5000); //数据库或服务器有问题
return result;
}
}
注册配置
在 config
目录创建 GlobalControllerPathPrefixConfig
类,写入以下内容:
// 省略 package import
//群集统一配置接口前缀
@Configuration
public class GlobalControllerPathPrefixConfig implements WebMvcConfigurer {
@Value("${spring.controller.api-prefix}")
private String pathPrefix;
@Value("${file.upload.path-pattern}")
private String pathPattern;
@Value("${file.upload.path}")
private String fileUploadPath;
//全局接口注册 api前缀
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix(pathPrefix, c -> c.isAnnotationPresent(RestController.class));
}
//静态资源图片上传,虚拟路径返回
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//将匹配上/files/**虚拟路径的url映射到文件上传到服务器的目录,获取静态资源
registry.addResourceHandler("/" +pathPattern + "/**").addResourceLocations("file:" + fileUploadPath+File.separator);
WebMvcConfigurer.super.addResourceHandlers(registry);
}
}
创建工具类
- 创建一个全局返回类。在
utils
里面创建一个ResJson
类型
// 省略 import
@Data
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
@Service
public class FileServerImpl implements FileServer {
public static final int maxWidth = 100;
//拦截的url,虚拟路径
public String pathPattern = "uploads";
//文件磁盘路径
@Value("${file.upload.path}")
private String fileUploadPath;
@Value(value = "${file.upload.suffix:jpg,jpeg,png,bmp,xls,xlsx,pdf}")
private String fileUploadSuffix;
@Value(value = "${file.upload.is-thumb}")
private Boolean isThumb;
@Value(value = "${file.upload.proportion}")
private Integer proportion;
@Autowired
HttpServletRequest request;
@Autowired
FileMapper fileMapper;
//文件上传
@Override
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;
}
//生成缩率图
@Override
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
@RestController
public class UploadController extends BaseController {
@Autowired
FileServer fileServer;
@PostMapping("/upload")
public ResJson<FileEntity> upload(@RequestParam MultipartFile file){
FileEntity fileRes= fileServer.upload(file);
return new ResJson<>(OK,"上传成功!",fileRes);
}
}
- 测试
通过 postman
接口测试,调用上面的上传接口
查看application.yml
里面配置的上传目录,是否有文件
通过网络路径访问图片
总结
本文适合 springboot
入门的初学者。
以文件上传为demo,衍生出了一些常用功能,包含:文件上传,入库,请求。异常处理,api前缀,数据迁移,生成缩略图,等功能。
希望能对初学者有一个参考的作用。
– 欢迎点赞、关注、转发、收藏【我码玄黄】,gonghao同名