SpringBoot+VUE+阿里云OSS实现简单的视频上传和展示

目录

一、功能介绍

实现了vue上传视频到阿里云oss,当上传视频的时候会查询是否上传过,如果上传过则返回,没有上传过则实现上传配合md5,利用ffmpeg实现自动配置封面图等功能。

二、配置阿里云OSS

1、阿里云OSS的相关依赖:

xml 复制代码
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.17.4</version>
        </dependency>

2、引入JavaCV相关依赖:

xml 复制代码
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv-platform</artifactId>
    <version>1.5.9</version>
</dependency>
xml 复制代码
aliyun:
  oss:
    # 1. endpoint - OSS服务端点
    endpoint: #####
    # 2. bucketName - 存储空间名称  
    bucketName: ####
    # 3. region - 地域节点
    region: cn-beijing
    # 4. accessKeyId - 访问密钥ID 
    accessKeyId: ####
    # 5. accessKeySecret - 访问密钥密钥
    accessKeySecret: ########

将上面的application.yaml添加上以上的配置信息。

三、定义阿里云OSS配置类文件

java 复制代码
package com.qcby.util;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
    private String endpoint;
    private String bucketName;
    private String region;
    private String accessKeyId;
    private String accessKeySecret;

    public String getEndpoint() {
        return endpoint;
    }

    public void setEndpoint(String endpoint) {
        this.endpoint = endpoint;
    }

    public String getBucketName() {
        return bucketName;
    }

    public void setBucketName(String bucketName) {
        this.bucketName = bucketName;
    }

    public String getRegion() {
        return region;
    }

    public void setRegion(String region) {
        this.region = region;
    }

    public String getAccessKeyId() {
        return accessKeyId;
    }

    public void setAccessKeyId(String accessKeyId) {
        this.accessKeyId = accessKeyId;
    }

    public String getAccessKeySecret() {
        return accessKeySecret;
    }

    public void setAccessKeySecret(String accessKeySecret) {
        this.accessKeySecret = accessKeySecret;
    }
}

使用@Component将类注册为Spring Bean,使用@ConfigurationProperties(prefix = "aliyun.oss")读取application.yml文件的配置信息。

四、阿里云OSS操作类

java 复制代码
package com.qcby.util;

import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import com.aliyun.oss.common.comm.SignVersion;
import com.aliyun.oss.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Component
public class AliyunOSSOperator {

    @Autowired
    private AliyunOSSProperties aliyunOSSProperties;

    public String upload(byte[] content, String originalFilename) {
        String endpoint = aliyunOSSProperties.getEndpoint();
        String bucketName = aliyunOSSProperties.getBucketName();
        String region = aliyunOSSProperties.getRegion();
        String accessKeyId = aliyunOSSProperties.getAccessKeyId();
        String accessKeySecret = aliyunOSSProperties.getAccessKeySecret();

        // 生成存储路径:yyyy/MM/uuid.扩展名
        String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
        String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
        String objectName = dir + "/" + newFileName;

        // 使用 DefaultCredentialProvider 直接传入 AK/SK
        DefaultCredentialProvider credentialProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);

        // 配置 OSS Client(可选,如启用 V4 签名)
        ClientBuilderConfiguration clientConfig = new ClientBuilderConfiguration();
        clientConfig.setSignatureVersion(SignVersion.V4);

        // 创建 OSSClient 实例
        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialProvider)  // 使用 AK/SK 凭证
                .clientConfiguration(clientConfig)
                .region(region)
                .build();

        try {
            // 上传文件
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));

            // 返回文件的访问 URL(如:https://bucket.endpoint/objectName)
            return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
        } finally {
            ossClient.shutdown();  // 关闭 OSSClient
        }
    }
    // 新的 InputStream 方法
    public String upload(InputStream inputStream, String originalFilename) {
        String endpoint = aliyunOSSProperties.getEndpoint();
        String bucketName = aliyunOSSProperties.getBucketName();
        String region = aliyunOSSProperties.getRegion();
        String accessKeyId = aliyunOSSProperties.getAccessKeyId();
        String accessKeySecret = aliyunOSSProperties.getAccessKeySecret();

        // 生成存储路径:yyyy/MM/uuid.扩展名
        String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
        String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
        String objectName = dir + "/" + newFileName;

        DefaultCredentialProvider credentialProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);

        ClientBuilderConfiguration clientConfig = new ClientBuilderConfiguration();
        clientConfig.setSignatureVersion(SignVersion.V4);

        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialProvider)
                .clientConfiguration(clientConfig)
                .region(region)
                .build();

        try {
            // 上传文件流
            ossClient.putObject(bucketName, objectName, inputStream);

            // 返回文件的访问 URL
            return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
        } finally {
            ossClient.shutdown();
        }
    }
    /**
     * 分片上传 - 适用于大文件
     */
    public String uploadMultipart(InputStream inputStream, String originalFilename, long fileSize) throws Exception {
        String endpoint = aliyunOSSProperties.getEndpoint();
        String bucketName = aliyunOSSProperties.getBucketName();
        String region = aliyunOSSProperties.getRegion();
        String accessKeyId = aliyunOSSProperties.getAccessKeyId();
        String accessKeySecret = aliyunOSSProperties.getAccessKeySecret();

        // 生成存储路径
        String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
        String newFileName = UUID.randomUUID() + getFileExtension(originalFilename);
        String objectName = dir + "/" + newFileName;

        DefaultCredentialProvider credentialProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
        ClientBuilderConfiguration clientConfig = new ClientBuilderConfiguration();
        clientConfig.setSignatureVersion(SignVersion.V4);

        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialProvider)
                .clientConfiguration(clientConfig)
                .region(region)
                .build();

        try {
            // 1. 初始化分片上传
            InitiateMultipartUploadRequest initiateRequest = new InitiateMultipartUploadRequest(bucketName, objectName);
            InitiateMultipartUploadResult initiateResult = ossClient.initiateMultipartUpload(initiateRequest);
            String uploadId = initiateResult.getUploadId();

            // 2. 设置分片大小(5MB)
            final long partSize = 5 * 1024 * 1024;
            List<PartETag> partETags = new ArrayList<>();

            // 3. 上传分片
            int partNumber = 1;
            long bytesRead = 0;
            byte[] buffer = new byte[(int) partSize];

            int bytes = 0;
            while ((bytes = inputStream.read(buffer)) != -1) {
                // 最后一个分片可能小于 partSize
                if (bytes < partSize) {
                    byte[] lastBuffer = new byte[bytes];
                    System.arraycopy(buffer, 0, lastBuffer, 0, bytes);
                    buffer = lastBuffer;
                }

                UploadPartRequest uploadPartRequest = new UploadPartRequest();
                uploadPartRequest.setBucketName(bucketName);
                uploadPartRequest.setKey(objectName);
                uploadPartRequest.setUploadId(uploadId);
                uploadPartRequest.setInputStream(new ByteArrayInputStream(buffer));
                uploadPartRequest.setPartSize(bytes);
                uploadPartRequest.setPartNumber(partNumber);

                UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
                partETags.add(uploadPartResult.getPartETag());

                System.out.println("上传分片 " + partNumber + " 完成, 大小: " + bytes + " bytes");
                partNumber++;
                bytesRead += bytes;
            }

            // 4. 完成分片上传
            CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
                    bucketName, objectName, uploadId, partETags);
            CompleteMultipartUploadResult completeResult = ossClient.completeMultipartUpload(completeRequest);

            // 返回文件的访问 URL
            return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;

        } catch (Exception e) {
            throw new Exception("分片上传失败: " + e.getMessage(), e);
        } finally {
            ossClient.shutdown();
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (Exception e) {
                    System.err.println("关闭输入流失败: " + e.getMessage());
                }
            }
        }
    }

    /**
     * 智能上传 - 根据文件大小自动选择上传方式
     */
    public String smartUpload(InputStream inputStream, String originalFilename, long fileSize) throws Exception {
        // 小于50MB使用简单上传,大于50MB使用分片上传
        long threshold = 50 * 1024 * 1024;

        if (fileSize <= threshold) {
            return upload(inputStream, originalFilename);
        } else {
            return uploadMultipart(inputStream, originalFilename, fileSize);
        }
    }

    /**
     * 获取文件扩展名
     */
    private String getFileExtension(String filename) {
        if (filename == null || filename.lastIndexOf(".") == -1) {
            return "";
        }
        return filename.substring(filename.lastIndexOf("."));
    }
}

将阿里云OSS配置文件类引入阿里云OSS操作类当中,执行upload上传到阿里云OSS文件中。

五、编写文件上传接口

java 复制代码
package com.qcby.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.qcby.dao.VideoDao;
import com.qcby.entity.Video;
import com.qcby.result.Result;
import com.qcby.util.AliyunOSSOperator;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Arrays;
import java.util.UUID;

//上传接口
@RestController
public class UploadController {
    @Autowired
    private AliyunOSSOperator aliyunOSSOperator;
    @Autowired
    private VideoDao videoDao;
    @RequestMapping("/upload")
    public Result uploadFile(MultipartFile file) throws Exception {
        String originalFilename = file.getOriginalFilename();
        byte[] bytes = file.getBytes();
        String url = aliyunOSSOperator.upload(bytes,originalFilename);
        return new Result().success(url);
    }
    @RequestMapping("/uploadFile")
    public Result uploadFile(@RequestParam("file") MultipartFile file,
                             @RequestParam("classId") Integer classId,
                             HttpServletRequest request) throws Exception {

        // 1. 基础验证
        if (file.isEmpty()) {
            return Result.error("文件不能为空");
        }

        // 2. 文件大小限制 (100MB)
        long maxSize = 100 * 1024 * 1024;
        System.out.println("文件大小:" + file.getSize());
        System.out.println("文件允许大小:" + maxSize);
        if (file.getSize() > maxSize) {
            return Result.error("文件大小不能超过100MB");
        }

        // 3. 文件类型验证
        String originalFilename = file.getOriginalFilename();
        String fileExtension = getFileExtension(originalFilename);
        String[] allowedExtensions = {"mp4", "avi", "mov", "wmv", "flv", "m4v"};

        if (!Arrays.asList(allowedExtensions).contains(fileExtension.toLowerCase())) {
            return Result.error("只支持视频文件格式:mp4, avi, mov, wmv, flv, m4v");
        }

        try {
            // 4. 计算文件MD5
            String md5Hash = calculateMD5(file);

            // 5. 检查文件是否已存在(避免重复上传)
            Video existingVideo = videoDao.selectOne(new QueryWrapper<Video>()
                    .eq("md5_hash", md5Hash)
                    .eq("class_id", classId)
                    .eq("status", 1));
            if (existingVideo != null) {
                return Result.success("文件已存在", existingVideo.getStoragePath());
            }
            Video video = new Video();
            // 6. 上传到OSS
            String url;
            try (InputStream inputStream = file.getInputStream()) {
                url = aliyunOSSOperator.upload(inputStream, originalFilename);
            }
            try {
                // 使用FFmpeg截取视频第一帧作为封面
                String coverUrl = extractVideoFrame(url);
                video.setCoverUrl(coverUrl);
                video.setCoverStatus(1); // 生成成功

            } catch (Exception e) {
                // 生成失败,使用默认封面
                video.setCoverUrl(getDefaultCoverUrl());
                video.setCoverStatus(2); // 生成失败
            }
            // 7. 获取视频时长(需要额外实现)
            Integer duration = getVideoDuration(file);
            System.out.println("视频时长:" + duration);
            // 8. 保存到数据库

            video.setOriginalName(originalFilename);
            video.setStoragePath(url);
            video.setFileSize(file.getSize());
            video.setDuration(duration);
            video.setMimeType(file.getContentType());
            video.setUploaderId(getCurrentUserId(request)); // 从session或token获取用户ID
            video.setStatus(1);
            video.setMd5Hash(md5Hash);
            video.setClassId(classId);

            videoDao.insert(video);

            return new Result().success("上传成功", url);

        } catch (Exception e) {
            return Result.error("上传失败: " + e.getMessage());
        }
    }

    // 辅助方法
    private String getFileExtension(String filename) {
        if (filename == null || filename.lastIndexOf(".") == -1) {
            return "";
        }
        return filename.substring(filename.lastIndexOf(".") + 1);
    }

    private String calculateMD5(MultipartFile file) throws Exception {
        try (InputStream is = file.getInputStream()) {
            return DigestUtils.md5DigestAsHex(is);
        }
    }

    // 获取视频时长(需要依赖)
    private Integer getVideoDuration(MultipartFile file) {
        File tempFile = null;
        try {
            // 创建临时文件
            tempFile = File.createTempFile("video_", "_temp");
            file.transferTo(tempFile);

            // 使用 FFmpegFrameGrabber 获取视频信息
            try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(tempFile)) {
                grabber.start();

                // 方法1:直接获取时长(微秒)
                long durationInMicroseconds = grabber.getLengthInTime();
                if (durationInMicroseconds > 0) {
                    int durationInSeconds = (int) (durationInMicroseconds / 1000000);
                    System.out.println("通过 getLengthInTime 获取时长: " + durationInSeconds + "秒");
                    return durationInSeconds;
                }

                // 方法2:通过帧率和总帧数计算
                double frameRate = grabber.getFrameRate();
                long totalFrames = grabber.getLengthInFrames();

                if (frameRate > 0 && totalFrames > 0) {
                    int durationInSeconds = (int) (totalFrames / frameRate);
                    System.out.println("通过帧率计算时长 - 总帧数: " + totalFrames + ", 帧率: " + frameRate + ", 时长: " + durationInSeconds + "秒");
                    return durationInSeconds;
                }

                // 方法3:尝试从格式上下文获取
                try {
                    double duration = grabber.getFormatContext().duration();
                    if (!Double.isNaN(duration) && duration > 0) {
                        int durationInSeconds = (int) (duration / 1000000);
                        System.out.println("通过 FormatContext 获取时长: " + durationInSeconds + "秒");
                        return durationInSeconds;
                    }
                } catch (Exception e) {
                    System.out.println("FormatContext 获取时长失败: " + e.getMessage());
                }

                grabber.stop();
                System.out.println("无法获取视频时长");
                return null;

            } catch (FrameGrabber.Exception e) {
                System.out.println("FrameGrabber 异常: " + e.getMessage());
                return null;
            }
        } catch (Exception e) {
            System.out.println("获取视频时长失败: " + e.getMessage());
            return null;
        } finally {
            // 删除临时文件
            if (tempFile != null && tempFile.exists()) {
                try {
                    tempFile.delete();
                } catch (Exception e) {
                    System.out.println("删除临时文件失败: " + e.getMessage());
                }
            }
        }
    }

    private Long getCurrentUserId(HttpServletRequest request) {
        // 从session、token或SecurityContext中获取当前用户ID
        // 示例:return (Long) request.getSession().getAttribute("userId");
//        TODO 获取当前用户ID
        return 1L; // 临时返回固定值
    }
    private String extractVideoFrame(String videoUrl) throws Exception {
        FFmpegFrameGrabber grabber = null;
        try {
            grabber = new FFmpegFrameGrabber(videoUrl);
            grabber.start();

            // 获取视频总帧数
            int totalFrames = grabber.getLengthInFrames();
            System.out.println("视频总帧数: " + totalFrames);

            // 选择提取帧的位置(例如:第10帧或视频的1/10处)
            int targetFrame = Math.min(10, totalFrames / 10);
            if (targetFrame > 0) {
                grabber.setFrameNumber(targetFrame);
            }

            // 获取帧
            Frame frame = grabber.grabImage();
            if (frame == null) {
                // 如果获取失败,尝试获取第一帧
                grabber.setFrameNumber(0);
                frame = grabber.grabImage();
            }

            if (frame == null) {
                throw new Exception("无法从视频中提取帧");
            }

            // 转换为 BufferedImage
            Java2DFrameConverter converter = new Java2DFrameConverter();
            BufferedImage image = converter.getBufferedImage(frame);

            // 调整图片尺寸(可选)
            image = resizeImage(image, 320, 180);

            // 转换为 InputStream 并上传
            InputStream imageStream = imageToInputStream(image, "jpg", 0.8f);
            String coverFileName = "covers/" + System.currentTimeMillis() + "_" + (int)(Math.random() * 1000) + ".jpg";
            String coverUrl = aliyunOSSOperator.upload(imageStream, coverFileName);

            return coverUrl;

        } finally {
            if (grabber != null) {
                try {
                    grabber.stop();
                } catch (Exception e) {
                    System.err.println("关闭FFmpegFrameGrabber失败: " + e.getMessage());
                }
            }
        }
    }

    /**
     * 调整图片尺寸
     */
    private BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) {
        BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
        java.awt.Graphics2D g = resizedImage.createGraphics();
        g.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
        g.dispose();
        return resizedImage;
    }

    /**
     * 将 BufferedImage 转换为 InputStream
     */
    private InputStream imageToInputStream(BufferedImage image, String format, float quality) throws Exception {
        ByteArrayOutputStream os = new ByteArrayOutputStream();

        try {
            // 使用ImageIO写入
            boolean result = ImageIO.write(image, format, os);
            if (!result) {
                // 如果指定格式不支持,尝试JPEG
                result = ImageIO.write(image, "jpg", os);
                if (!result) {
                    throw new Exception("不支持的图片格式: " + format);
                }
            }

            return new ByteArrayInputStream(os.toByteArray());

        } finally {
            try {
                os.close();
            } catch (Exception e) {
                System.err.println("关闭ByteArrayOutputStream失败: " + e.getMessage());
            }
        }
    }
    private String getDefaultCoverUrl() {
        // 返回默认封面图URL
        return "https://lyf-java-ai.oss-cn-beijing.aliyuncs.com/20250428/1745853671798234.jpg";
    }
}

简单的上传接口:@RequestMapping("/upload"),视频文件上传接口:@RequestMapping("/uploadFile")

六、Video相关配置

1、Video实体类

java 复制代码
package com.qcby.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;

@Data
@TableName("video")
public class Video {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String originalName;
    private String storagePath;
    private Long fileSize;
    private Integer duration;
    private String mimeType;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date uploadTime;
    private Long uploaderId;
    private Integer status;
    private String md5Hash;
    private Integer classId;
    private String coverUrl;
    private Integer coverStatus;
}

2、Video数据库和Mapper层

数据库表定义如下:

sql 复制代码
CREATE TABLE `video`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT,
  `original_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '原始文件名',
  `storage_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '存储路径',
  `file_size` bigint(0) NOT NULL COMMENT '文件大小(字节)',
  `duration` int(0) DEFAULT NULL COMMENT '视频时长(秒)',
  `mime_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '文件类型',
  `upload_time` datetime(0) DEFAULT CURRENT_TIMESTAMP,
  `uploader_id` bigint(0) DEFAULT NULL COMMENT '上传者ID',
  `status` tinyint(0) DEFAULT 1 COMMENT '状态:1正常,0删除',
  `md5_hash` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '文件MD5哈希',
  `class_id` int(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;
java 复制代码
package com.qcby.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.qcby.entity.Video;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface VideoDao extends BaseMapper<Video> {
}
相关推荐
艾小码1 小时前
Vue开发三年,我才发现依赖注入的TypeScript正确打开方式
前端·javascript·vue.js
Evan Wang2 小时前
深度解析GetX依赖注入,从Spring与Vue视角看Flutter架构
vue.js·spring boot·flutter
后端小张4 小时前
【JAVA进阶】Spring Boot 核心知识点之自动配置:原理与实战
java·开发语言·spring boot·后端·spring·spring cloud·自动配置
3***C7449 小时前
Spring Boot 整合 log4j2 日志配置教程
spring boot·单元测试·log4j
X***C8629 小时前
SpringBoot:几种常用的接口日期格式化方法
java·spring boot·后端
i***t9199 小时前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
8***848210 小时前
spring security 超详细使用教程(接入springboot、前后端分离)
java·spring boot·spring
o***741710 小时前
基于SpringBoot的DeepSeek-demo 深度求索-demo 支持流式输出、历史记录
spring boot·后端·lua
9***J62810 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端