springboot基础及上传组件封装

简介

本文主要以文件上传为demo,介绍了一些 springboot web 开发的入门的技术栈。

对应刚接触 springboot 的可以参考下。

主要包括文件md5比对、生成图片缩略图、数据库迁移、文件记录持久化、请求全局异常处理等功能。

准备工作

  • idea 中创建项目,java8 , springboot 2
  • maven 所需依赖
xml 复制代码
<?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 配置:
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   # 当前环境
  • 创建数据表迁移文件

写入以下内容:

sql 复制代码
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 写入以下内容:

java 复制代码
// 省略 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 写入以下内容:

java 复制代码
// 省略 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 接口类
java 复制代码
// 省略 package import 

@Mapper
public interface FileMapper extends BaseMapper<FileEntity> {

    FileEntity queryByMd5(String md5);
}
  • 创建 mapper xml 文件。在 resources 目录下面创建 mapper 目录,然后再创建 FileMapper.xml。并写入以下内容
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 类,编写如下代码:
java 复制代码
// 省略 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 继承他
java 复制代码
// 省略 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 类,写入以下内容:

java 复制代码
// 省略 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 类型
java 复制代码
// 省略 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 类型
java 复制代码
// 省略 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 接口类
java 复制代码
// 省略 package import 

public interface FileServer {

    FileEntity upload(MultipartFile file);

    String createThumb(String fileDir,String filePath,String fileName,String suffix);

}
  • server目录里面创建impl目录并在里面创建FileServerImpl类实现上面的接口功能
java 复制代码
// 省略 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,写入以下内容:
java 复制代码
// 省略 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同名

相关推荐
小黄编程快乐屋1 小时前
各个排序算法基础速通万字介绍
java·算法·排序算法
kingwebo'sZone1 小时前
ASP.net WebAPI 上传图片实例(保存显示随机文件名)
后端·asp.net
桑榆肖物1 小时前
一个简单的ASP.NET 一致性返回工具库
后端·asp.net
材料苦逼不会梦到计算机白富美3 小时前
贪心算法-区间问题 C++
java·c++·贪心算法
组态软件4 小时前
web组态软件
前端·后端·物联网·编辑器·html
Peter_chq4 小时前
【计算机网络】多路转接之select
linux·c语言·开发语言·网络·c++·后端·select
小小李程序员7 小时前
LRU缓存
java·spring·缓存
cnsxjean7 小时前
SpringBoot集成Minio实现上传凭证、分片上传、秒传和断点续传
java·前端·spring boot·分布式·后端·中间件·架构
hadage2337 小时前
--- stream 数据流 java ---
java·开发语言
《源码好优多》7 小时前
基于Java Springboot汽配销售管理系统
java·开发语言·spring boot