2026山东大学软件学院项目实训(五)——AI应用生成之应用封面图功能完整实现
·
标签
#AI编程 #Java #SpringBoot #Selenium #网页截图 #对象存储
引言
上一节我们完成了AI应用生成模块的核心能力开发,实现了从提示词到代码的流式生成、应用与代码绑定、权限校验等核心功能。在实际平台化场景中,应用预览是提升用户体验、增强平台专业性的关键一环——如果每个生成的应用都有精美的预览封面,用户无需打开网页就能直观看到效果。
本节实训我们聚焦应用封面图自动生成功能,基于Selenium无头浏览器实现网页自动截图,结合腾讯云COS对象存储完成图片持久化,最终实现“部署应用→自动截图→上传云端→更新数据库→清理本地文件”的全流程闭环。下面从设计思路、技术选型、代码实现、测试验证四个维度,完整复盘整个开发过程。
一、模块整体设计思路
1. 核心价值
- 所见即所得:直接截取应用运行时页面作为封面,真实还原效果
- 自动化:无需人工干预,部署完成后异步自动生成,不阻塞主流程
- 轻量化:截图压缩后上传,兼顾清晰度与存储成本
- 可持久化:云端存储封面图,支持跨设备访问,本地不残留冗余文件
2. 技术选型对比
封面生成核心是网页截图,主流技术方案对比如下:
| 技术方案 | 依赖大小 | 启动时间 | 内存占用 | JS支持 | 推荐度 |
|---|---|---|---|---|---|
| Selenium | ~50MB | 3-5秒 | 200-500MB | 完整支持 | ★★★★★ |
| Playwright | ~100MB | 1-2秒 | 100-300MB | 完整支持 | ★★★★☆ |
| HtmlUnit | ~30MB | <1秒 | 20-50MB | 有限支持 | ★★☆☆☆ |
| Puppeteer | ~200MB | 2-3秒 | 150-400MB | 完整支持 | ★★★☆☆ |
| 云服务API | 0MB | 网络延迟 | 0MB | 服务商决定 | ★★★☆☆ |
最终选型:Selenium
适配我们Java技术栈,成熟稳定、完整支持JS渲染,能兼容所有生成的前端页面,搭配WebDriverManager可自动管理浏览器驱动,大幅降低配置成本。
3. 核心业务流程
- 用户完成AI应用部署,生成可访问的应用URL
- 异步触发封面生成任务(不阻塞部署流程)
- Selenium无头浏览器访问应用URL,等待页面完全加载(含JS渲染)
- 截取页面完整截图,对图片进行压缩处理
- 将压缩后的图片上传至腾讯云COS对象存储
- 从COS获取图片访问URL,更新数据库中应用表的
cover字段 - 清理本地临时截图文件,避免磁盘占用
二、核心代码改造与实现
1. 引入项目依赖
在pom.xml中引入Selenium、WebDriverManager、腾讯云COS、Hutool等核心依赖:
<!-- Selenium 网页截图核心依赖 -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.33.0</version>
</dependency>
<!-- WebDriverManager 自动管理Chrome驱动 -->
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>6.1.0</version>
</dependency>
<!-- 腾讯云COS 对象存储依赖 -->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.227</version>
</dependency>
<!-- Hutool 工具库(文件、压缩、IO操作) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
2. 开发网页截图工具类
在utils包下创建WebScreenshotUtils,实现驱动初始化、截图、压缩、文件清理等核心能力:
package com.pt.aicodeformal.utils;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.pt.aicodeformal.exception.BusinessException;
import com.pt.aicodeformal.exception.ErrorCode;
import io.github.bonigarcia.wdm.WebDriverManager;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.io.File;
import java.time.Duration;
import java.util.UUID;
@Slf4j
public class WebScreenshotUtils {
// 线程隔离:每个线程独有WebDriver,避免并发冲突
private static final ThreadLocal<WebDriver> DRIVER_THREAD_LOCAL = new ThreadLocal<>();
// 截图窗口默认尺寸
private static final int DEFAULT_WIDTH = 1600;
private static final int DEFAULT_HEIGHT = 900;
// 图片压缩质量(30%,平衡清晰度和大小)
private static final float COMPRESSION_QUALITY = 0.3f;
/**
* 初始化Chrome无头浏览器驱动
*/
private static WebDriver initChromeDriver(int width, int height) {
try {
// 自动下载并匹配Chrome驱动
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
// 新版无头模式(替代旧版--headless)
options.addArguments("--headless=new");
// 环境兼容配置
options.addArguments("--disable-gpu");
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
// 设置窗口大小
options.addArguments(String.format("--window-size=%d,%d", width, height));
// 禁用扩展、优化性能
options.addArguments("--disable-extensions");
// 设置用户代理
options.addArguments("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36");
WebDriver driver = new ChromeDriver(options);
// 超时配置
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
return driver;
} catch (Exception e) {
log.error("初始化Chrome浏览器失败", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "初始化Chrome浏览器失败");
}
}
/**
* 获取当前线程驱动,复用不重复创建
*/
private static WebDriver getDriver() {
WebDriver driver = DRIVER_THREAD_LOCAL.get();
if (driver == null) {
driver = initChromeDriver(DEFAULT_WIDTH, DEFAULT_HEIGHT);
DRIVER_THREAD_LOCAL.set(driver);
}
return driver;
}
/**
* 关闭当前线程驱动并清理引用
*/
public static void closeDriver() {
WebDriver driver = DRIVER_THREAD_LOCAL.get();
if (driver != null) {
try {
driver.quit();
} catch (Exception e) {
log.error("关闭浏览器进程异常", e);
} finally {
DRIVER_THREAD_LOCAL.remove();
}
}
}
/**
* 等待页面完全加载(含JS渲染)
*/
private static void waitForPageLoad(WebDriver driver) {
try {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(webDriver ->
((JavascriptExecutor) webDriver).executeScript("return document.readyState")
.equals("complete")
);
// 额外等待2秒,确保动态内容渲染完成
Thread.sleep(2000);
log.info("页面加载完成");
} catch (Exception e) {
log.warn("等待页面加载超时,继续执行截图");
}
}
/**
* 生成网页截图并返回压缩后的本地路径
* @param webUrl 网页访问URL
* @return 压缩后图片路径
*/
public static String saveWebPageScreenshot(String webUrl) {
if (StrUtil.isBlank(webUrl)) {
log.error("网页URL不能为空");
return null;
}
WebDriver driver = null;
try {
// 创建临时目录
String rootPath = System.getProperty("user.dir") + File.separator + "tmp" + File.separator + "screenshots"
+ File.separator + UUID.randomUUID().toString().substring(0, 8);
FileUtil.mkdir(rootPath);
// 获取当前线程浏览器
driver = getDriver();
// 访问网页并等待加载
driver.get(webUrl);
waitForPageLoad(driver);
// 截图并保存原始图片
String originalPath = rootPath + File.separator + RandomUtil.randomNumbers(5) + ".png";
byte[] screenshotBytes = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
FileUtil.writeBytes(screenshotBytes, originalPath);
// 压缩图片
String compressedPath = rootPath + File.separator + RandomUtil.randomNumbers(5) + "_compressed.jpg";
ImgUtil.compress(FileUtil.file(originalPath), FileUtil.file(compressedPath), COMPRESSION_QUALITY);
// 删除原始图片
FileUtil.del(originalPath);
return compressedPath;
} catch (Exception e) {
log.error("网页截图失败:{}", webUrl, e);
return null;
} finally {
// 释放驱动资源
closeDriver();
}
}
/**
* 清理本地临时截图目录
* @param filePath 截图文件路径
*/
public static void cleanupTempFile(String filePath) {
if (StrUtil.isBlank(filePath)) {
return;
}
File file = new File(filePath);
if (file.exists()) {
File parentDir = file.getParentFile();
FileUtil.del(parentDir);
log.info("清理本地临时截图目录:{}", parentDir.getAbsolutePath());
}
}
}
3. 腾讯云COS配置
3.1 配置文件(application.yml)
# 腾讯云COS配置
cos:
client:
host: cos.ap-shanghai.myqcloud.com
secret-id: 你的腾讯云secretId
secret-key: 你的腾讯云secretKey
region: ap-shanghai
bucket: 你的存储桶名称
3.2 COS配置类(CosClientConfig.java)
package com.pt.aicodeformal.config;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.region.Region;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "cos.client")
public class CosClientConfig {
private String host;
private String secretId;
private String secretKey;
private String region;
private String bucket;
@Bean
public COSClient cosClient() {
COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
ClientConfig clientConfig = new ClientConfig(new Region(region));
return new COSClient(cred, clientConfig);
}
}
3.3 COS工具类(CosManager.java)
package com.pt.aicodeformal.manager;
import cn.hutool.core.util.StrUtil;
import com.pt.aicodeformal.config.CosClientConfig;
import com.pt.aicodeformal.exception.BusinessException;
import com.pt.aicodeformal.exception.ErrorCode;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.model.PutObjectResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Slf4j
@Component
public class CosManager {
@Resource
private CosClientConfig cosClientConfig;
@Resource
private COSClient cosClient;
/**
* 上传文件到COS并返回访问URL
*/
public String uploadFile(String key, File file) {
if (StrUtil.isBlank(key) || file == null || !file.exists()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "上传参数无效");
}
try {
PutObjectRequest request = new PutObjectRequest(cosClientConfig.getBucket(), key, file);
PutObjectResult result = cosClient.putObject(request);
String url = String.format("%s/%s", cosClientConfig.getHost(), key);
log.info("文件上传COS成功:{} -> {}", file.getName(), url);
return url;
} catch (Exception e) {
log.error("文件上传COS失败", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "文件上传COS失败");
}
}
/**
* 生成COS存储路径(按日期分层)
*/
public String generateScreenshotKey(String fileName) {
String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
return String.format("/screenshots/%s/%s", datePath, fileName);
}
}
4. 截图服务封装
package com.pt.aicodeformal.service;
public interface ScreenshotService {
/**
* 生成网页截图、上传COS、清理本地文件
* @param webUrl 网页URL
* @return 封面图访问URL
*/
String generateAndUploadScreenshot(String webUrl);
}
package com.pt.aicodeformal.service.impl;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.pt.aicodeformal.exception.BusinessException;
import com.pt.aicodeformal.exception.ErrorCode;
import com.pt.aicodeformal.manager.CosManager;
import com.pt.aicodeformal.service.ScreenshotService;
import com.pt.aicodeformal.utils.WebScreenshotUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
@Slf4j
@Service
public class ScreenshotServiceImpl implements ScreenshotService {
private final CosManager cosManager;
public ScreenshotServiceImpl(CosManager cosManager) {
this.cosManager = cosManager;
}
@Override
public String generateAndUploadScreenshot(String webUrl) {
if (StrUtil.isBlank(webUrl)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "网页URL不能为空");
}
log.info("开始生成网页截图:{}", webUrl);
// 1. 生成本地截图
String localPath = WebScreenshotUtils.saveWebPageScreenshot(webUrl);
if (StrUtil.isBlank(localPath)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "本地截图生成失败");
}
try {
// 2. 上传COS
File screenshotFile = new File(localPath);
String fileName = RandomUtil.randomNumbers(8) + "_compressed.jpg";
String cosKey = cosManager.generateScreenshotKey(fileName);
String cosUrl = cosManager.uploadFile(cosKey, screenshotFile);
log.info("网页截图生成并上传成功:{} -> {}", webUrl, cosUrl);
return cosUrl;
} finally {
// 3. 清理本地文件
WebScreenshotUtils.cleanupTempFile(localPath);
}
}
}
5. 异步触发截图(应用部署后调用)
在AppService的deployApp方法末尾,添加异步截图逻辑:
@Resource
private ScreenshotService screenshotService;
@Override
public String deployApp(Long appId, User loginUser) {
// ... 原有部署逻辑,生成应用访问URL ...
String appDeployUrl = "http://localhost/" + deployKey + "/";
// 异步生成封面(Java21虚拟线程,不阻塞主线程)
Thread.startVirtualThread(() -> {
try {
String coverUrl = screenshotService.generateAndUploadScreenshot(appDeployUrl);
// 更新数据库应用封面
App app = new App();
app.setId(appId);
app.setCover(coverUrl);
this.updateById(app);
log.info("应用封面更新成功,appId:{}", appId);
} catch (Exception e) {
log.error("异步生成应用封面失败,appId:{}", appId, e);
}
});
return appDeployUrl;
}
三、功能测试与验证
1. 环境准备
- 服务器安装Chrome浏览器(Selenium依赖)
- 腾讯云COS创建存储桶,配置
secretId/secretKey - 项目配置文件填入COS信息,启动SpringBoot项目
2. 测试步骤
- 调用
addApp接口创建应用 - 调用
chatToGenCode接口生成网页代码 - 调用
deployApp接口部署应用,获取访问URL - 等待10秒(异步任务执行)
- 查询数据库
app表,cover字段已填充COS访问URL - 浏览器打开
cover对应URL,可正常查看封面图
3. 常见问题解决
- Chrome驱动下载失败:配置国内镜像,添加系统属性
static { System.setProperty("wdm.mirror", "https://npmmirror.com/mirrors/chromedriver/"); } - 无头模式启动失败:Docker环境添加
--no-sandbox参数,服务器关闭图形界面 - COS上传失败:检查密钥权限、存储桶地域配置、网络连通性
- 本地文件残留:确保
finally块执行文件清理逻辑
四、总结与后续优化
1. 本次实训收获
- 掌握Selenium无头浏览器实现网页自动截图的核心技术
- 理解对象存储(腾讯云COS)的文件上传、路径管理流程
- 学会Java21虚拟线程实现异步任务,避免阻塞主业务
- 掌握临时文件清理、资源释放、并发安全的设计思路
2. 功能价值
封面生成功能让AI应用生成平台从“能用”升级到“好用”,大幅提升用户使用意愿,同时实现了截图自动化、存储云端化、流程闭环化,符合平台化产品的设计理念。
3. 后续优化方向
- 定时清理COS过期截图,节省存储成本
- 配置默认封面图,截图失败时兜底展示
- 结合AI智能路由,根据应用复杂度调整截图尺寸
- 增加截图质量动态配置,适配不同网络环境
更多推荐

所有评论(0)