简介:这是一套基于SpringBoot的Java视频点播系统完整课程设计源码,面向计算机专业本科生、Java初学者及Web全栈学习者,聚焦于后端开发、数据库交互与多媒体业务集成能力训练。资源包含312个文件,涵盖76个Java核心业务类(Controller/Service/Repository分层清晰)、59个Vue前端组件(实现视频浏览、搜索与播放界面)、40个JS与19个CSS文件支撑交互逻辑与样式,另有SQL建表脚本、YML配置、MP4演示视频及多尺寸图片资源,整体包体16.92MB,结构完整、开箱即用。已有45人学习下载,适合用于课程设计答辩、毕业设计参考或SpringBoot工程化实践。读者可直接获取含用户管理、视频上传存储、多清晰度转码适配、Redis缓存优化及Spring Security权限控制的全流程实现,代码注释充分,目录模块划分明确,配套SQL与部署说明便于快速本地运行与二次开发。

1. 为什么一个 SpringBoot 视频点播系统,不能只靠“能跑起来”就交差?

很多 Java 开发者在做毕业设计或内部 PoC 时,看到“SpringBoot-Java视频点播系统设计与实现.zip”这个标题,第一反应是解压、 mvn clean install 、 java -jar target/*.jar ,然后打开浏览器输入 http://localhost:8080 —— 页面出来了,上传个 MP4 能播,就以为完成了。但真实业务场景里,这连及格线都没摸到:用户上传 500MB 视频卡死?HLS 切片没生成,移动端直接黑屏?同一视频并发 200+ 请求,CPU 拉满、磁盘 IO 告警?后台管理页连视频封面都没自动提取?更别说权限控制漏掉管理员删除他人视频、HTTP 头缺失导致跨域失败、日志里全是 NullPointerException 却找不到源头……这个标题背后不是“做个能播的网页”,而是要落地一套 可监控、可伸缩、可运维、符合生产级媒体服务规范的 Java 后端服务 。它面向的是需要交付、上线、被 QA 和运维盯住的开发者,不是仅满足课程答辩的演示工程。核心矛盾从来不是“能不能播”,而是“播得稳不稳、查得清不清、扩得快不快、改得安不安”。接下来,我们就从 SpringBoot 的能力边界出发,一层层把视频点播的骨架搭实、血肉填满、神经接通。

2. 用 SpringBoot 构建点播服务的最小可行架构:为什么选 WebMvc + FFmpeg + LocalStorage 而非全栈云方案

2.1 架构选型必须回答三个现实问题:部署成本、调试确定性、技术栈收敛性

很多初学者一上来就想集成阿里云 OSS、腾讯云点播 API 或自建 SRS 流媒体服务器,结果卡在 AK/SK 配置、跨域策略、回调地址白名单、HTTPS 证书绑定上,两周过去连本地上传都跑不通。而本项目标题明确指向“设计与实现”,意味着 所有组件必须可本地复现、可单机验证、可断网调试 。SpringBoot 本身不处理音视频编解码,也不提供流式分发能力,但它作为胶水层,能精准调度外部工具、统一管理元数据、暴露 RESTful 接口。因此我们采用三层极简架构:

  • 接入层 :SpringBoot WebMvc(非 WebFlux,因点播文件 I/O 密集,阻塞式更易控)
  • 处理层 :FFmpeg CLI(版本 6.1,静态编译版,避免系统依赖冲突)
  • 存储层 :本地文件系统( /data/vod/ ),配合 ResourceHandler 直接映射 HTTP 路径,规避对象存储 SDK 版本兼容陷阱

提示:不要用 spring-boot-starter-webflux 替代 spring-boot-starter-web 。WebFlux 在大文件上传时需额外配置 DataBuffer 大小和超时,且 FFmpeg 进程调用本质是阻塞操作,强行 Reactive 反而增加线程调度开销和内存泄漏风险。

2.2 初始化 SpringBoot 工程:避开 JDK 17+ 和 SpringBoot 3.x 的常见陷阱

当前热词中高频出现“springboot版本太高”,确为痛点。本项目需稳定运行 FFmpeg 进程并支持 multipart/form-data 大文件上传,推荐组合:

# 使用 Spring Initializr 创建时关键选项:
# Group: com.example.vod
# Artifact: vod-server
# Spring Boot: 2.7.18(LTS,兼容 JDK 8–17)
# Dependencies: Spring Web, Spring Data JPA, Lombok, Validation

若强制使用 SpringBoot 3.x(如面试题常考的 @ConfigurationProperties 新语法),则必须升级至 JDK 17+,且 FFmpeg 调用需显式指定 ProcessBuilder.inheritIO() 避免子进程日志丢失:

// SpringBoot 3.x 兼容写法(JDK 17+)
ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-i", inputPath, "-c:v", "libx264", "-f", "mp4", outputPath);
pb.inheritIO(); // 关键!否则 FFmpeg 错误输出无法捕获
Process process = pb.start();
int exitCode = process.waitFor();

而 SpringBoot 2.7.x 下可直接用 Runtime.getRuntime().exec() ,代码更直白:

// SpringBoot 2.7.x 推荐写法(JDK 8+)
String[] cmd = {
    "ffmpeg", "-y",
    "-i", input.getAbsolutePath(),
    "-vcodec", "libx264",
    "-acodec", "aac",
    "-f", "mp4",
    output.getAbsolutePath()
};
Process proc = Runtime.getRuntime().exec(cmd);
BufferedReader errorReader = new BufferedReader(
    new InputStreamReader(proc.getErrorStream(), StandardCharsets.UTF_8)
);
String line;
while ((line = errorReader.readLine()) != null) {
    log.warn("FFmpeg error: {}", line); // 实时捕获错误,便于定位编码失败原因
}
int exitCode = proc.waitFor();

2.3 视频上传接口的健壮性设计:不只是 @RequestParam MultipartFile file

单纯接收 MultipartFile 会导致 OOM(上传 1GB 文件时 JVM 堆内存爆掉)。必须启用流式上传并限制单文件大小:

# application.yml
spring:
  servlet:
    context-path: /api
  servlet:
    multipart:
      max-file-size: 2GB
      max-request-size: 2GB
      # 关键:禁用内存缓冲,强制写入临时磁盘
      file-size-threshold: 0
server:
  tomcat:
    # Tomcat 默认 2MB,必须调大
    max-swallow-size: -1
    connection-timeout: 600000 # 10分钟超时,避免大文件上传中断

对应 Controller 层需手动处理输入流,而非依赖 MultipartFile.transferTo() :

@PostMapping("/upload")
public ResponseEntity<UploadResult> uploadVideo(@RequestParam String title,
                                               @RequestParam String category,
                                               HttpServletRequest request) throws IOException {
    // 1. 从 request 获取原始 InputStream(绕过 MultipartFile 内存拷贝)
    ServletInputStream inputStream = request.getInputStream();
    String originalFilename = getOriginalFilename(request); // 从 Content-Disposition 解析
    
    // 2. 生成唯一存储路径(避免中文路径乱码)
    String safeName = FilenameUtils.normalize(originalFilename, true);
    String videoId = UUID.randomUUID().toString().replace("-", "");
    Path storagePath = Paths.get("/data/vod/raw/", videoId + "_" + safeName);
    
    // 3. 流式写入磁盘(不加载全量到内存)
    Files.createDirectories(storagePath.getParent());
    try (FileOutputStream fos = new FileOutputStream(storagePath.toFile())) {
        inputStream.transferTo(fos);
    }
    
    // 4. 异步触发转码(避免阻塞 HTTP 线程)
    CompletableFuture.runAsync(() -> transcodeVideo(videoId, storagePath));
    
    return ResponseEntity.ok(new UploadResult(videoId, "uploading"));
}
2.3.1 getOriginalFilename() 的安全实现:防御恶意文件名
private String getOriginalFilename(HttpServletRequest request) {
    String contentDisposition = request.getHeader("Content-Disposition");
    if (contentDisposition == null) return "unknown.mp4";
    Pattern pattern = Pattern.compile("filename[^;]*=\\s*\"([^\"]*)\"|filename[^;]*=\\s*([^;]*)");
    Matcher matcher = pattern.matcher(contentDisposition);
    String filename = matcher.find() ? matcher.group(1) != null ? matcher.group(1) : matcher.group(2) : "unknown.mp4";
    // 移除路径遍历字符(..、/、\)和控制字符
    return filename.replaceAll("[\\\\/\\x00-\\x1f\\x7f]", "_").replaceAll("\\.{2,}", "_");
}

3. 视频转码与 HLS 切片:用 FFmpeg 命令链打通点播核心链路

3.1 为什么必须生成 HLS(.m3u8 + .ts)而非仅提供 MP4 直播链接?

热词中“springboot vue前后端分离”高频出现,意味着前端大概率用 video.js 或 hls.js 播放。而现代浏览器(Chrome/Firefox/Safari)对 MP4 的 range 请求支持虽好,但 无首帧预加载、无自适应码率、无 CDN 缓存友好性 。HLS 是事实标准: .m3u8 是播放清单, .ts 是分片视频,CDN 可缓存每个 .ts 文件,用户拖动进度时只需加载对应分片,而非整个 MP4。SpringBoot 不负责生成这些文件,但必须可靠调用 FFmpeg 完成。

3.2 生产可用的 FFmpeg 转码命令模板(含关键参数说明)

以下命令在 CentOS 7/Ubuntu 22.04 实测通过,适配主流硬件:

ffmpeg -y \
  -i "/data/vod/raw/abc123_video.mp4" \
  -c:v libx264 \
  -preset medium \
  -crf 23 \
  -maxrate 1500k \
  -bufsize 3000k \
  -vf "scale='if(gt(iw,ih),min(1280,iw),-2)':'if(gt(iw,ih),-2,min(720,ih))'" \
  -c:a aac \
  -b:a 128k \
  -ac 2 \
  -ar 44100 \
  -f hls \
  -hls_time 10 \
  -hls_list_size 0 \
  -hls_segment_filename "/data/vod/hls/abc123_%05d.ts" \
  "/data/vod/hls/abc123.m3u8"
参数 说明 为什么必须设
-preset medium 编码速度/质量平衡点 ultrafast 画质差、 slow 耗时长, medium 是生产默认值
-crf 23 恒定质量因子(0-51,越小越好) 18 画质过优浪费带宽, 28 压缩过度, 23 是 YouTube 推荐值
-maxrate 1500k + -bufsize 3000k VBR(可变码率)控制 防止瞬时码率飙升导致卡顿, bufsize 应为 maxrate 的 2 倍
-vf scale=... 自适应分辨率缩放 保证宽高比不变,最大宽度 1280px/高度 720px,避免手机端拉伸
-hls_time 10 每个 .ts 分片时长(秒) <5s 增加 HTTP 请求压力, >15s 拖动延迟高, 10s 是 CDN 友好值
-hls_list_size 0 保留全部 .ts 文件 避免 HLS 清理旧分片导致播放中断

3.3 SpringBoot 中安全执行 FFmpeg 并捕获进度

FFmpeg 执行耗时长(1GB 视频约 3–8 分钟),需异步 + 进度上报:

private void transcodeVideo(String videoId, Path rawPath) {
    try {
        Path hlsDir = Paths.get("/data/vod/hls/", videoId);
        Files.createDirectories(hlsDir);
        
        String m3u8Path = hlsDir.resolve(videoId + ".m3u8").toString();
        String tsPattern = hlsDir.resolve(videoId + "_%05d.ts").toString();
        
        String[] cmd = {
            "ffmpeg", "-y",
            "-i", rawPath.toString(),
            "-c:v", "libx264", "-preset", "medium", "-crf", "23",
            "-maxrate", "1500k", "-bufsize", "3000k",
            "-vf", "scale='if(gt(iw,ih),min(1280,iw),-2)':'if(gt(iw,ih),-2,min(720,ih))'",
            "-c:a", "aac", "-b:a", "128k", "-ac", "2", "-ar", "44100",
            "-f", "hls", "-hls_time", "10", "-hls_list_size", "0",
            "-hls_segment_filename", tsPattern, m3u8Path
        };
        
        ProcessBuilder pb = new ProcessBuilder(cmd);
        pb.redirectErrorStream(true); // 合并 stdout/stderr,便于解析进度
        Process proc = pb.start();
        
        // 实时读取 FFmpeg 输出(含进度百分比)
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(proc.getInputStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains("frame=")) {
                    // 解析如:frame= 2345 fps= 24.1 q=-1.0 size= 123456kB time=00:01:32.45 bitrate=1234.5kbits/s
                    double progress = parseProgress(line);
                    updateTranscodeProgress(videoId, progress); // 存入 Redis 或 DB
                }
            }
        }
        
        int exitCode = proc.waitFor();
        if (exitCode == 0) {
            markTranscodeSuccess(videoId, m3u8Path);
        } else {
            markTranscodeFailed(videoId, "FFmpeg exit code: " + exitCode);
        }
    } catch (Exception e) {
        log.error("Transcode failed for {}", videoId, e);
        markTranscodeFailed(videoId, e.getMessage());
    }
}

private double parseProgress(String ffmpegOutput) {
    // 匹配 time=00:01:32.45 提取总秒数
    Pattern p = Pattern.compile("time=(\\d{2}):(\\d{2}):(\\d{2}\\.\\d{2})");
    Matcher m = p.matcher(ffmpegOutput);
    if (m.find()) {
        int hours = Integer.parseInt(m.group(1));
        int mins = Integer.parseInt(m.group(2));
        double secs = Double.parseDouble(m.group(3));
        return (hours * 3600 + mins * 60 + secs) / DURATION_ESTIMATE; // 需预估总时长
    }
    return 0.0;
}

注意: DURATION_ESTIMATE 不能硬编码。应在上传后先用 ffprobe 快速获取时长: ffprobe -v quiet -show_entries format=duration -of csv=p=0 input.mp4 ,再启动转码。否则进度条不准。

4. 视频元数据管理与播放地址生成:用 JPA + MySQL 实现可查询的点播目录

4.1 数据库表设计必须覆盖点播业务核心维度

仅存 video_id , file_path 远不够。用户要按分类筛选、按热度排序、看播放统计、查审核状态。MySQL 表结构如下(Spring Data JPA Entity):

@Entity
@Table(name = "vod_video")
@Data
public class VideoEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "video_id", unique = true, nullable = false, length = 32)
    private String videoId; // UUID,非自增ID,防爬虫遍历

    @Column(name = "title", nullable = false, length = 200)
    private String title;

    @Column(name = "category", nullable = false, length = 50)
    private String category; // "education", "entertainment", "tech"

    @Column(name = "status", nullable = false, length = 20)
    @Enumerated(EnumType.STRING)
    private VideoStatus status; // UPLOADED, TRANSCODING, READY, FAILED

    @Column(name = "duration_sec", nullable = false)
    private Integer durationSec; // 秒数,用于前端显示时长

    @Column(name = "cover_url", length = 500)
    private String coverUrl; // /api/cover/{videoId}.jpg

    @Column(name = "hls_m3u8_url", length = 500)
    private String hlsM3u8Url; // /api/hls/{videoId}.m3u8

    @Column(name = "view_count", nullable = false)
    private Long viewCount = 0L;

    @Column(name = "created_at", nullable = false, updatable = false)
    @CreationTimestamp
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false)
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

public enum VideoStatus {
    UPLOADED, TRANSCODING, READY, FAILED
}

4.2 播放地址生成规则:拒绝硬编码路径,用 SpringBoot ResourceHandler 统一托管

前端请求 /api/hls/abc123.m3u8 ,不应由 Controller 返回文件流(性能差),而应交由 SpringBoot 静态资源处理器:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 映射 /api/hls/** 到 /data/vod/hls/ 目录
        registry.addResourceHandler("/api/hls/**")
                .addResourceLocations("file:/data/vod/hls/")
                .setCachePeriod(3600); // 缓存 1 小时,减少磁盘读

        // 映射封面图
        registry.addResourceHandler("/api/cover/**")
                .addResourceLocations("file:/data/vod/cover/")
                .setCachePeriod(86400); // 封面图长期缓存
    }
}

这样, VideoEntity.hlsM3u8Url 字段只需存相对路径 /api/hls/abc123.m3u8 ,前端直接 <video src="/api/hls/abc123.m3u8"> 即可。 避免 Controller 层做 ResponseEntity<Resource> 包装,那是反模式 。

4.3 查询接口的性能优化:分页 + 条件 + 缓存

用户列表页需支持分类筛选、按热度排序、分页:

@Repository
public interface VideoRepository extends JpaRepository<VideoEntity, Long> {
    // JPQL 自定义查询,避免 N+1
    @Query("SELECT v FROM VideoEntity v WHERE v.status = 'READY' " +
           "AND (:category IS NULL OR v.category = :category) " +
           "ORDER BY v.viewCount DESC")
    Page<VideoEntity> findReadyVideos(@Param("category") String category, Pageable pageable);

    // 精确查询单个视频(用于播放页)
    Optional<VideoEntity> findByVideoId(String videoId);
}

Controller 层开启二级缓存(Ehcache 或 Caffeine):

@Service
public class VideoService {
    @Cacheable(value = "videos", key = "#videoId", unless = "#result == null")
    public VideoEntity getVideoById(String videoId) {
        return videoRepository.findByVideoId(videoId)
                .orElseThrow(() -> new RuntimeException("Video not found: " + videoId));
    }
}
# application.yml 启用缓存
spring:
  cache:
    type: caffeine
    cache-names: videos
  caffeine:
    spec: maximumSize=1000,expireAfterWrite=10m

5. 播放统计与防盗链:用拦截器 + Referer 校验守住流量出口

5.1 为什么播放统计不能只靠前端埋点?

用户刷新页面、关闭标签页、网络中断都会导致统计不准。必须在服务端记录每次 .m3u8 和 .ts 请求:

@Component
public class VideoAccessInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String path = request.getRequestURI();
        if (path.startsWith("/api/hls/")) {
            String videoId = extractVideoIdFromPath(path);
            if (videoId != null) {
                // 记录访问(异步,避免阻塞)
                CompletableFuture.runAsync(() -> {
                    videoStatsService.incrementViewCount(videoId);
                    // 可扩展:记录 IP、User-Agent、Referer
                });
            }
        }
        return true;
    }

    private String extractVideoIdFromPath(String path) {
        // /api/hls/abc123.m3u8 → abc123
        Pattern p = Pattern.compile("/api/hls/([a-zA-Z0-9]+)\\.m3u8");
        Matcher m = p.matcher(path);
        return m.find() ? m.group(1) : null;
    }
}

注册拦截器:

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new VideoAccessInterceptor())
                .excludePathPatterns("/api/upload", "/api/health");
    }
}

5.2 防盗链:用 Referer 白名单堵住外站盗链

热词中“java知道了服务器的公网怎么访问”暗示了公网暴露风险。若不限制 Referer,别人只需在自己网站 <video src="https://your-domain.com/api/hls/xxx.m3u8"> 就能白嫖你的带宽:

@Component
public class RefererCheckFilter implements Filter {
    private static final Set<String> ALLOWED_REFERERS = Set.of(
        "https://your-frontend.com",
        "http://localhost:3000",
        "http://127.0.0.1:3000"
    );

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String referer = httpRequest.getHeader("Referer");
        String path = httpRequest.getRequestURI();

        if (path.startsWith("/api/hls/") && referer != null) {
            boolean allowed = false;
            for (String domain : ALLOWED_REFERERS) {
                if (referer.startsWith(domain)) {
                    allowed = true;
                    break;
                }
            }
            if (!allowed) {
                HttpServletResponse httpResponse = (HttpServletResponse) response;
                httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Referer not allowed");
                return;
            }
        }
        chain.doFilter(request, response);
    }
}

提示:生产环境必须用 Nginx 做前置 Referer 校验(更高效),Java 层拦截器仅作兜底。Nginx 配置示例:

location /api/hls/ {
    valid_referers none blocked server_names ~\.your-frontend\.com;
    if ($invalid_referer) {
        return 403;
    }
    alias /data/vod/hls/;
}

5.3 封面图自动生成:用 FFmpeg 抽帧 + Java 图像处理

用户上传视频后,系统应自动生成封面(第 1 秒关键帧),而非让用户手动上传:

public void generateCover(String videoId, Path videoPath) {
    try {
        Path coverDir = Paths.get("/data/vod/cover/");
        Files.createDirectories(coverDir);
        Path coverPath = coverDir.resolve(videoId + ".jpg");

        // 抽取第 1 秒帧
        String[] cmd = {
            "ffmpeg", "-y",
            "-ss", "00:00:01.000", // 精确到毫秒
            "-i", videoPath.toString(),
            "-vframes", "1",
            "-q:v", "2", // 质量 2(1-31,越小越好)
            coverPath.toString()
        };
        Process proc = Runtime.getRuntime().exec(cmd);
        proc.waitFor();

        // 可选:用 Thumbnailator 缩放为 320x180(适配移动端)
        Thumbnails.of(coverPath.toFile())
                .size(320, 180)
                .outputQuality(0.9)
                .toFile(coverPath.toFile());

        // 更新数据库
        videoRepository.findByVideoId(videoId)
                .ifPresent(v -> {
                    v.setCoverUrl("/api/cover/" + videoId + ".jpg");
                    videoRepository.save(v);
                });
    } catch (Exception e) {
        log.error("Cover generation failed for {}", videoId, e);
    }
}

6. 生产部署 checklist:从开发机到 Linux 服务器的 7 个必调项

6.1 JVM 参数:针对 I/O 密集型服务的堆外内存与 GC 优化

SpringBoot 默认 JVM 参数不适合视频服务。 -Xmx 设太大反而触发 Full GC,太小则 FFmpeg 进程创建失败:

# 启动脚本 start.sh
#!/bin/bash
java \
  -Xms512m -Xmx1024m \          # 堆内存 0.5–1G 足够,避免大堆 GC
  -XX:MetaspaceSize=256m \      # 元空间初始值
  -XX:+UseG1GC \                # G1 垃圾收集器,低延迟
  -XX:MaxGCPauseMillis=200 \    # GC 暂停目标 200ms
  -Dfile.encoding=UTF-8 \       # 防止中文路径乱码
  -Dsun.net.client.defaultConnectTimeout=60000 \
  -Dsun.net.client.defaultReadTimeout=60000 \
  -jar vod-server.jar \
  --spring.profiles.active=prod

6.2 Linux 系统级调优:突破文件句柄与进程限制

# /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536
* soft nproc 65536
* hard nproc 65536

# /etc/sysctl.conf
fs.file-max = 2097152
net.core.somaxconn = 65535
vm.swappiness = 1

6.3 FFmpeg 安装验证:确保静态编译版无 glibc 依赖

# 在目标服务器执行
ldd $(which ffmpeg) | grep "not found"  # 若有输出,则需换静态版
# 推荐下载:https://johnvansickle.com/ffmpeg/ (x86_64 静态版)
wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
tar -xf ffmpeg-release-amd64-static.tar.xz
sudo cp ffmpeg-*/ffmpeg /usr/local/bin/

6.4 日志分级:区分业务日志与 FFmpeg 调试日志

# logback-spring.xml
<configuration>
  <appender name="FFMPEG_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/var/log/vod/ffmpeg.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>/var/log/vod/ffmpeg.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
      <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
        <maxFileSize>100MB</maxFileSize>
      </timeBasedFileNamingAndTriggeringPolicy>
    </rollingPolicy>
  </appender>

  <logger name="com.example.vod.service.FFmpegService" level="WARN" additivity="false">
    <appender-ref ref="FFMPEG_LOG"/>
  </logger>
</configuration>

6.5 健康检查端点:让运维能快速判断服务状态

@RestController
@RequestMapping("/actuator")
public class HealthController {
    @GetMapping("/health")
    public Map<String, Object> health() {
        Map<String, Object> result = new HashMap<>();
        result.put("status", "UP");
        result.put("timestamp", System.currentTimeMillis());
        result.put("disk_free", FileStoreUtil.getFreeSpace("/data/vod")); // 自定义工具类
        result.put("ffmpeg_version", getFFmpegVersion()); // 执行 ffmpeg -version
        result.put("db_status", checkDatabaseConnection());
        return result;
    }
}

6.6 文件存储权限:避免 java.io.IOException: Permission denied

# 创建专用用户运行服务
sudo useradd -r -s /bin/false voduser
sudo chown -R voduser: /data/vod/
sudo chmod -R 755 /data/vod/

# 启动服务时指定用户
sudo -u voduser ./start.sh

6.7 HTTPS 强制跳转:Nginx 前置配置(非 SpringBoot 内置)

server {
    listen 80;
    server_name vod.your-domain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name vod.your-domain.com;

    ssl_certificate /etc/letsencrypt/live/vod.your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vod.your-domain.com/privkey.pem;

    location /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api/hls/ {
        alias /data/vod/hls/;
        add_header Cache-Control "public, max-age=3600";
        expires 1h;
    }
}

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

邀请您加入社区

更多推荐