当前位置: 首页 > article >正文

SpringBoot和Vue的图片上传的解决方案

本文章的概述

前端上传图片,后端接收和存储图片,以及前端渲染图片的一套常规的解决方案

涉及的技术什么?

前端: vue , elementPlus , axios

后端: springboot配置资源访问映射 ,  springboot.MultipartFile类接收文件对象

第三方: springboot集成阿里云oss对象存储


大概的流程是什么?

如图所示: 图1是本文章即将探讨的图片上传解决方案,图2是另外一种解决方案(只是贴上来了解即可,和本文章核心内容无关)

前端接受用户的一个图片,并且通过接口传递给服务端,服务端拿到图片需要做2件事情

1.立即存储图片 并 得到一个可访问图片的url链接

2.给该图片建立相关的业务逻辑关系,即:生成id 将id和url存储同一个表中,并将id存储在另外的关联表中,方便服务端后续自己拿id得到图片链接 然后返回给前端 用作访问图片

(图1:)

(图2:)


具体代码细节实现 : java的每个类 每个配置 每个操作 每个js文件 ....

贸然贴代码不可取,我们这里带入一个小的功能需求:
"

用户可以发布新增一个文章,文章包含:img字段,id字段,title字段等....

用户发布新增文章成功之后,可以点击文章编辑按钮 并查看到文章详情

"

功能如下图所示:

用户新增文章:

用户编辑文章时 可以回显图片:

首先前端 定义组件 这里使用了 element的el-upload组件

        <el-form-item label="文章图片上传">
          <!-- auto-upload是否开启自动上传?  show-file-list是否展示历史上传的记录? on-change当上传图片至组件时会触发的方法-->
          <el-upload
              class="avatar-uploader"
              :auto-upload="false"
              :show-file-list="true"
              :on-change="onUploadFile"
          >
            <!--v-if和src当formModel.imgUrl有值时则渲染该图片链接 alt当imgUrl无法访问时,展示的文字  -->
            <img alt="这个图片丢失了喵~" v-if="formModel.imgUrl" :src="formModel.imgUrl" class="avatar"/>
            <!--v-else 当formModel.imgUrl无值时则渲染一个"+"号-->
            <el-icon v-else class="avatar-uploader-icon">
              <Plus/>
            </el-icon>
          </el-upload>
        </el-form-item>

注意了哈 上面图片渲染时 使用的是formModel.imgUrl,这意味着所有可直接访问的图片链接 都应该交给此值

也就是说后端返回的文章对象的imgUrl需要绑定给formModel.imgUrl,而前端本地临时浏览的图片对象也要绑定给formModel.imgUrl,

formModel定义如下

//新增或更新的表单 用于提交给服务端
const formModel = ref({
  id: "",
  name: "",
  categoryId: "",
  content: "",
  status: "",
  imgUrl: "",
})

上传图片前端代码定义

//单独使用const绑定绑定图片文件,这样的话等下容易进行后端接口传递
const imgModel = ref({
  imgFile: null,
})
//当el-upload组件的图片上传时触发的逻辑
const onUploadFile = async (file) => {
  ElMessage.success("选择了一个图片")
  //1.绑定到表单 等下提交给后端
  imgModel.value.imgFile = file.raw
  //2.使用浏览器自带的方法URL.createObjectURL() 将文件对象转换为一个浏览器本地可访问的url图片链接对象
  formModel.value.imgUrl = URL.createObjectURL(file.raw)
  ElMessage.success("本地渲染图片成功:" + formModel.value.imgUrl)
}

至此 前端上传图片和渲染图片的静态逻辑定义完毕

接下来定义前端请求后端时 提交表单的逻辑

//点击新增或修改文章按钮的逻辑
const emit = defineEmits(["success"])
const onInsertOrUpdate = async () => {
  ElMessage.success("新增或修改文章")
  //有id则是修改
  if (formModel.value.id) {
    //将formModel绑定的文章的普通数据 和 文章的图片file 分为2个参数传递给后端
    await updateArticleService(formModel.value, imgModel.value.imgFile)
    ElMessage.success("修改成功")
    emit("success")
    showForm.value = false;
    return
  }
  //没有id则是新增
  await insertArticleService(formModel.value, imgModel.value.imgFile)
  emit("success")
  ElMessage.success("新增成功")
  showForm.value = false;
}

还有前端请求后端时的axiosApi封装的代码如下

//请求服务端 新增文章
export const insertArticleService = (articleForm, file) => {
    //使用formData表单对象 来传递给服务端参数 对应springboot的@RequestParams
    const formData = new FormData();
    //将表单转为json传递 
    formData.append('formJson', JSON.stringify(articleForm));
    //传递文件对象
    formData.append('file', file);
    //请求
    myAxios.post("/ArticleController/insert", formData).then()
};
//请求服务端 更新文章
export const updateArticleService = (articleForm, file) => {
    const formData = new FormData();
    formData.append('formJson', JSON.stringify(articleForm));
    //如果用户不点击图片上传逻辑的话 组件传递过来的file对象是null,如果是null的话,服务端那边的springBoot接口就会报400 bad request
    //因此这里需要判断一下file是不是null 如果是 则将其设置为一个空的txt文件
    if (file) {
        formData.append('file', file);
    }else {
        // 创建一个空的 Blob 对象,指定文件名匿名为空文件
        const emptyFile= new Blob([], { type: 'application/octet-stream' });
        formData.append('file', emptyFile, 'empty.txt'); // 'empty.txt' 是文件名,你可以根据需要更改
    }
    myAxios.post("/ArticleController/update", formData).then()
};

至此 前端的上传图片,本地预览图片,和点击按钮请求后端新增图片功能完毕

接下来是后端的接口定义如下:

这里主要是接受json并转为服务端的java对象 以及接收前端传递的file文件对象

    //新增文章
    @PostMapping("insert")
    public Result<Boolean> insert(
            @RequestParam(value = "formJson") String formJson,
            @RequestParam(value = "file") MultipartFile file
    ){
        ArticleForm form = RoleJson.to(formJson,ArticleForm.class);
        articleService.insert(form,file);
        return Result.success();
    }

    //修改文章
    @PostMapping("update")
    public Result<Boolean> update(
            @RequestParam(value = "formJson") String formJson,
            @RequestParam(value = "file") MultipartFile file
    ){
        ArticleForm form = RoleJson.to(formJson,ArticleForm.class);
        articleService.update(form,file);
        return Result.success();
    }

我们不必要关心article文章对象的其他字段是如何存储的 只需要关心后端是如何操作file文件即可

因此这里只展示一下insert方法,如下

    public void insert(ArticleForm form, MultipartFile file) {
        ArticlePo po = new ArticlePo();

        //设置 id name
        po.setId(RoleId.idUUIDTrim());
        po.setName(form.getName());
        //设置 分类id
        po.setCategoryId(form.getCategoryId());

        //设置 imgId
        Result<String> stringResult = imgFileUploadingController.handleFileUpload(file);
        String data = stringResult.getData();
        String id = imgStorageService.insert(data);
        po.setImg(id);

        //设置 富文本
        po.setContent(form.getContent());
        //设置 文章发布状态
        po.setStatus(form.getStatus());

        //存入数据库
        RoleMybatisPlus.insert(mapper,po);
    }

最主要就是设置imgId这里 ,  我们是如何将file文件对象 变成 string类型的id呢 ?

这里会带出几个问题

1.直接将整个file文件序列号转为二进制存储在数据库可以吗? 可以,但不优雅

2.将file对象转为url链接之后 再存储到数据库可以吗? 可以 比第一个方案优雅 但是不规范 会造成图片字段散落在各个表中

因此我们采取的做法是将file对象先转为url链接 再生成1个id 再将id和url绑定在一个img_storage关系表中,这样其他业务表:比如article文章表的imgId字段就可以存储img_storage表的id 依然可以关联到图片的访问url 最终返回给前端的组件进行渲染

因此

img_storage表定义如下

article文章表最终存储如下

那么现在让我们回归最初的问题; 如何将file文件变成可访问的url链接呢 ?

我们有2种方案 

第一种: 基于阿里云的OSS对象存储 , 优点是:简单好用且稳定,缺点是,如果图片要被公网流量访问就需要钱

第二种: 将文件存储在本地服务器的文件夹上,并且设置相关资源映射 ,优点是:不要钱 缺点是实现较为复杂

我们先来实现第一种 基于阿里云OSS存储文件 需要如下操作

阿里云OSS视图化页面配置

1.创建bucket,得到bucket名称 ①

2.查看bucket详情 得到Endpoint②

2.创建accessKey ID 和 accessKey Secret ③ ④

至此阿里云视图化页面配置完毕 现在你已经得到了以下4个参数

endpoint: oss-cn-hangzhou.aliyuncs.com
bucket-name: xiaokeerservice
access-key-id: LTAI5tQ*********bhmjPZup
access-key-secret: EkBgLcV************WpDwaNv

然后我们进入spring boot配置

1.引入aliyun.sdk.oss依赖

        <!--阿里云OSS文件上传-->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>${aliyun.sdk.oss}</version> <!--<aliyun.sdk.oss>3.18.1</aliyun.sdk.oss>-->
        </dependency>

2.application.yml中定义配置

xiaokeer:
  uploading:
     select: "1"
  #阿里云OSS相关配置 @see:AliOssProperties.java
  alioss:
     endpoint: oss-cn-hangzhou.aliyuncs.com
     bucket-name: xiaokeerservice
     access-key-id: LTAI5tQeEqrRkgStbhmjPZup
     access-key-secret: EkBgLcVyYkiiZNKp8FIiVqaWpDwaNv

3 定义配置类

/**
 * 阿里云OSS文件配置属性
 */
@Component
@ConfigurationProperties(prefix = "xiaokeer.alioss")
@Data
public class AliOssProperties {

    /**
     * 1.阿里服务器地址 2.bucket文件夹名称 3.授权秘钥id 4.授权秘钥值
     */
    private String endpoint;
    private String bucketName;
    private String accessKeyId;
    private String accessKeySecret;

}

4.定义阿里云OSS工具 并使用配置


/**
 * 阿里云OSS文件上传类
 */
@Component
public class RoleAliOss {

    @Resource
    private AliOssProperties aliOssProperties;

    /**
     * 文件上传
     *  1.ossClient 创建OSS代理对象 需要指定必要属性 (1.endpoint服务器名 2.accessKeyId授权秘钥id 3.accessKeySecret授权秘钥值)
     *  2.代理对象调用putObject方法 准备上传文件 传入参数(1.bucket文件夹名称 2.文件字节流bytes)
     *  3.出现异常需要打印异常信息 
     */
    public String upload(byte[] bytes, String objectName) {
        OSS ossClient = new OSSClientBuilder().build(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret());
        try {
            ossClient.putObject(aliOssProperties.getBucketName(), objectName, new ByteArrayInputStream(bytes));
        } catch (Exception e) {
            throw XiaoKeerException.newException(e);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        //当文件上传成功后 需要返回文件的访问路径 visitPath , 阿里云OSS文件的访问路径由以下部分组成 : "https://" + bucket文件名称 + "." + 服务器名称 + "/" + 文件全名
        return "https://" + aliOssProperties.getBucketName() + "." + aliOssProperties.getEndpoint() + "/" + objectName;
    }
}

5.使用阿里云OSS上传

@PostMapping("/upload")
    public Result<String> handleFileUpload(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            throw XiaoKeerException.newInvalidParamsException();
        }
        //生成新的唯一文件名
        String newFileName = RoleFile.newFileName(file);
        //1.阿里云OSS存储
        if(uploadingSelect.equals("1")){
            try {
                /*
                 * 调用aliOssUtil工具对象的upload方法 传入文件的字节数组 以及新的文件名
                 * 会将文件上传至阿里云OSS 最后返回文件的访问路径
                 */
                String OSSPath = aliOssUtil.upload(file.getBytes(), newFileName);
                return Result.success(OSSPath);
            } catch (Exception e) {
                throw XiaoKeerException.newException(e);
            }
        }
        throw XiaoKeerException.newUnknownBizException();
    }

至此 阿里云OSS集成完毕 我们传入file文件 得到一个url地址 我们可以在表中给这个url建立关联关系 也可以之间返回给前端 让其访问

那么 再来说一下第二种文件上传方式 : 上传到服务器的文件夹

1.首先在你服务器选一个风水宝地的文件夹位置 我这里假设项目已经部署到服务器了 我选择了当前项目src同等级下新建了一个upload_file.img文件夹

2.然后定义我们的上传方法

    public static final String UPLOADED_IMG_FILE_PATH = System.getProperty("user.dir") + "\\uploaded_file\\img\\";

      //2.当前服务器文件夹存储
        if(uploadingSelect.equals("2")){
            try {
                // 获取项目的根目录,并将文件保存到目标目录
                Path filePath = Paths.get( UPLOADED_IMG_FILE_PATH, newFileName);
                Files.write(filePath, file.getBytes());
                //todo 上传完毕之后 如何得到url呢?
            } catch (Exception e) {
                throw XiaoKeerException.newException(e);
            }
        }

3.那么问题来了 文件已经上传完毕 该如何得到文件的url访问链接呢?

我们继续补全方法如下

    //浏览img文件时的路径
    public static final String VISIT_IMG_FILE_PATH = "/xiaokeer_resource/uploaded_file/img/";
    //img文件上传的路径
    public static final String UPLOADED_IMG_FILE_PATH = System.getProperty("user.dir") + "\\uploaded_file\\img\\";

  //2.当前服务器文件夹存储
        if(uploadingSelect.equals("2")){
            try {
                // 获取项目的根目录,并将文件保存到目标目录
                Path filePath = Paths.get( UPLOADED_IMG_FILE_PATH, newFileName);
                Files.write(filePath, file.getBytes());
                // 构造静态文件访问路径
                String fileAccessUrl = "http://localhost:8081" + VISIT_IMG_FILE_PATH + newFileName;
                // 返回文件访问路径
                return Result.success(fileAccessUrl);
            } catch (Exception e) {
                throw XiaoKeerException.newException(e);
            }
        }

4.最终我们返回给前端的是这样的url地址

http://localhost:8081/xiaokeer_resource/uploaded_file/img/635898103123283968.png

5. ok问题来了  现在这种地址如果前端直接拿到服务端来访问 肯定是不通的 会被报404

原因很简单 这个地址确实不存在 , 我们的文件实际上存储在了 服务器的这个位置

D:\xiaokeer\xiaokeerServer\uploaded_file\img\635898103123283968.png

6.这时候前端就要暴躁了"你tm上传的位置和你返回的位置不一样啊喂 我怎么访问! "

别急别急 我们还差最后一步资源映射的配置 如下


@Configuration
public class WebConfig implements WebMvcConfigurer {


    //资源路径映射
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //将     http://localhost:8081/xiaokeer_resource/uploaded_file/635864958617391104.png
        //映射为: D:/xiaokeer/xiaokeerServer/uploaded_file/img/635864958617391104.png
        registry
                .addResourceHandler(
                        ImgFileUploadingController.VISIT_IMG_FILE_PATH + "*.jpg",
                                    ImgFileUploadingController.VISIT_IMG_FILE_PATH + "*.png" )
                .addResourceLocations("file:" + ImgFileUploadingController.UPLOADED_IMG_FILE_PATH);
    }

}

这样的话 前端就可以美滋滋的拿这样的路径

http://localhost:8081/xiaokeer_resource/uploaded_file/img/635898103123283968.png

来访问我们服务器文件夹的资源了  而且又避免了 我们将服务器的文件夹暴露给外边

至此文章结束

踩坑

1 body和file同时请求controller接口

一般来说如果有body入参和文件入参 还是采用这样的写法吧

    //新增文章
    @PostMapping("insert")
    public Result<Boolean> insert(
            @RequestParam(value = "formJson") String formJson,
            @RequestParam(value = "file") MultipartFile file
    ){
        ArticleForm form = RoleJson.to(formJson,ArticleForm.class);
        articleService.insert(form,file);
        return Result.success();
    }

2 将文件存储到静态文件夹中 借助spring自己的映射

在编写代码初期 我是将文件们存储在springboot的resource文件夹下的

也就是这些个位置

存储在这里的好处就是  你不用自己再去定义WebMvcConfigurer的映射配置了 

因为这地方 spring有默认的映射规则 大概如下

spring:
  servlet:
    multipart:
      max-file-size: 10MB # 设置controller上传单个文件的大小为10M
      max-request-size: 50MB # 设置controller上传总上传的数据大小为50M
  mvc:
    #定义访问静态文件时,必须携带的前缀
    #例如:http://localhost:8081/xiaokeer_resource/uploaded_file/img/cat.png
    #最终这个访问到的是: src\main\resources\resources\uploaded_file\img\cat.png
    #相当于 "http://localhost:8081/xiaokeer_resource/" 这一节 直接被替换成了: src\main\resources\
    #然后拼接 ["my_static_resource"或者"resources" 或者 "static" ...(按照下面配置的顺序)] 再拼接 "/uploaded_file/img/cat.png"
    #最终得到"src\main\resources\resources\uploaded_file\img\cat.png"
    static-path-pattern: /xiaokeer_resource/**
  web:
    resources:
      #定义所有存放静态资源的文件夹们 默认从这些文件夹里面找文件
      #例如:http://localhost:8081/xiaokeer_resource/cat.png
      #最终得到的就是 src\main\resources\resources\cat.png这个
      #因为my_static_resource没有叫这个的 , 然后就访问resources,发现了cat.png
      static-locations:
        - classpath:/my_static_resource/
        - classpath:/resources/
        - classpath:/static/
        - classpath:/public/

也就意味着  你可以直接拿一个这样的链接

http://localhost:8081/xiaokeer_resource/uploaded_file/img/cat.png

然后你就能直接访问到 src\main\resources\resources\uploaded_file\img\cat.png这个位置

听起来很方便是不是  但是这里有坑 因为这些地方 resource/resources resource/public resource/resources/static 它是存储静态资源的地方 不是存储业务文件的地方 , 所以在项目启动之后 这地方的文件会被编译到target文件夹中 也就是这里

而spring所有的映射访问 都是在访问这里的文件  并不是我们原始上传的src文件夹下的那里

所以最终就会导致 你上传了一个图片成功了 但是去访问的时候被报404 然后你需要重新启动一下springboot项目 你就又发现 图片可以正常访问了  这就是因为springboot需要编译target文件夹

因此最终结论就是 这地方不要存储业务的file对象 


http://www.kler.cn/news/360335.html

相关文章:

  • Java爬虫API:获取商品详情数据的利器
  • 基于SpringBoot+Vue+uniapp微信小程序的教学质量评价系统的详细设计和实现
  • 构建可扩展、安全和智能的数字化解决方案:微服务架构与物联网的深度融合
  • gitlab项目转移群组
  • 汇编实现逆序复制数据
  • 物联网防爆气象站的工作原理
  • C07.L10.STL之队列
  • 【推导过程】常用离散分布的数学期望、方差、特征函数
  • 物流行业创新:SpringBoot技术应用
  • 明日周刊-第23期
  • AI赋能大尺度空间模拟与不确定性分析及数字制图
  • docker compose部署mongodb 分片集群
  • 新版idea菜单栏展开与合并
  • 机器视觉基础系列四—简单了解背景建模算法
  • AI 通俗解读统计学和机器学习的主要区别
  • [AWS]RDS数据库版本升级
  • idea和webstorm性能优化
  • uniapp开发钉钉小程序踩坑记录...
  • Qt设置浏览器为父窗口,嵌入播放器窗口
  • 【Linux】进程信号(上)