1. 项目概述
最近用Spring Boot 1.4.1开发了一个音乐播放器系统,前后花了两个月时间从零开始搭建。这个项目让我对JavaEE技术栈有了更深入的理解,特别是Spring Boot如何简化传统SSM框架的配置过程。系统采用MVC分层架构,前端用Bootstrap实现响应式布局,后端整合了MyBatis和JPA两种ORM方案,数据库用的MySQL,还引入了Redis做缓存优化。
这个播放器最让我自豪的是它的性能表现——在2核4G的云服务器上实测能稳定支持500+并发用户在线播放。下面我会详细分享整个开发过程中的技术选型、架构设计和踩过的坑,希望能给想用Java做多媒体应用的开发者一些参考。
2. 技术选型与架构设计
2.1 为什么选择Spring Boot 1.4.1
虽然现在Spring Boot已经发展到3.x版本,但我选择1.4.1主要基于几个考虑:
- 项目需要兼容JDK1.8环境(客户服务器限制)
- 1.4.x版本已经具备自动配置、嵌入式Tomcat等核心特性
- 相比新版更轻量,启动内存减少约30%
- 社区资源丰富,遇到问题容易找到解决方案
实际开发中,通过@SpringBootApplication一个注解就替代了传统SSM项目中大量的XML配置。比如数据库连接池的配置,原来在XML中需要这样:
xml复制<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<!-- 其他配置项... -->
</bean>
现在只需要在application.properties中:
properties复制spring.datasource.url=jdbc:mysql://localhost:3306/music_db
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
2.2 前端技术栈选择
前端采用Bootstrap 3 + jQuery组合,主要考虑因素:
- 响应式布局自动适配手机/PC
- 组件丰富(特别是音频播放控件)
- 与Thymeleaf模板引擎无缝集成
播放器核心控件用的是HTML5的audio标签:
html复制<audio id="player" controls>
<source src="/music/{{song.filePath}}" type="audio/mpeg">
</audio>
配合jQuery实现播放列表功能:
javascript复制$('.play-btn').click(function(){
var songId = $(this).data('id');
$.get('/api/song/'+songId, function(data){
$('#player source').attr('src', '/music/'+data.filePath);
document.getElementById('player').load();
document.getElementById('player').play();
});
});
2.3 数据库设计要点
MySQL表设计遵循几个原则:
- 音乐表与分类表多对多关系(通过中间表实现)
- 用户收藏使用复合主键(user_id + song_id)
- 建立适当的索引提升查询性能
核心表结构示例:
sql复制CREATE TABLE `song` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
`artist` varchar(50) NOT NULL,
`duration` int(11) DEFAULT NULL,
`file_path` varchar(255) NOT NULL,
`cover_img` varchar(255) DEFAULT NULL,
`play_count` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_title` (`title`),
KEY `idx_artist` (`artist`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 核心功能实现
3.1 音乐播放模块
播放功能看似简单,实际开发中遇到几个关键问题:
-
音频格式兼容性:
- 测试发现不同浏览器支持的格式不同
- 解决方案:上传时用FFmpeg转码为MP3格式
bash复制
ffmpeg -i input.wav -codec:a libmp3lame -qscale:a 2 output.mp3 -
播放进度保存:
- 用户再次播放时可以从上次位置继续
- 实现方案:用localStorage存储播放进度
javascript复制// 存储进度 player.addEventListener('timeupdate', function() { localStorage.setItem('song_'+songId, player.currentTime); }); // 读取进度 var savedTime = localStorage.getItem('song_'+songId); if(savedTime) player.currentTime = savedTime; -
跨域问题:
- 当音频文件放在不同域名下时遇到CORS限制
- 解决方法:配置Nginx添加响应头
nginx复制location /music/ { add_header Access-Control-Allow-Origin *; alias /data/music/; }
3.2 用户收藏系统
收藏功能需要考虑并发问题:
java复制@Transactional
public boolean toggleFavorite(Integer userId, Integer songId) {
// 检查是否已收藏
Favorite exist = favoriteMapper.selectByUserAndSong(userId, songId);
if(exist != null) {
// 取消收藏
favoriteMapper.delete(exist.getId());
songMapper.decreaseFavoriteCount(songId);
return false;
} else {
// 添加收藏
Favorite fav = new Favorite();
fav.setUserId(userId);
fav.setSongId(songId);
fav.setCreateTime(new Date());
favoriteMapper.insert(fav);
songMapper.increaseFavoriteCount(songId);
return true;
}
}
注意:这里必须加@Transactional注解保证两个操作的原子性,否则可能出现收藏记录和计数不一致的情况。
3.3 后台管理系统
管理员模块的几个关键点:
-
批量上传:
- 使用Apache Commons FileItem处理多文件上传
- 每个文件单独开线程处理转码
-
数据统计:
- 每日凌晨用Spring Scheduled跑统计任务
java复制@Scheduled(cron = "0 0 3 * * ?") public void dailyStat() { // 统计昨日播放量、新增用户等 } -
敏感操作日志:
- 用AOP记录管理员的关键操作
java复制@Aspect @Component public class AdminLogAspect { @AfterReturning("execution(* com..admin.*.delete*(..))") public void logDelete(JoinPoint jp) { // 记录操作日志 } }
4. 性能优化实践
4.1 Redis缓存策略
针对高并发场景做了三级缓存:
- 热门歌曲信息缓存(过期时间1小时)
- 用户个性化推荐缓存(过期时间1天)
- 排行榜数据缓存(每10分钟更新)
缓存穿透解决方案:
java复制public Song getSongById(Integer id) {
// 1. 先查缓存
String key = "song:" + id;
Song song = redisTemplate.opsForValue().get(key);
if(song != null) {
return song;
}
// 2. 查数据库
song = songMapper.selectById(id);
if(song == null) {
// 防止缓存穿透:缓存空值5分钟
redisTemplate.opsForValue().set(key, new Song(), 5, TimeUnit.MINUTES);
return null;
}
// 3. 写入缓存
redisTemplate.opsForValue().set(key, song, 1, TimeUnit.HOURS);
return song;
}
4.2 数据库优化
-
索引优化:
- 为所有查询条件添加合适索引
- 联合查询建立复合索引
-
SQL优化:
- 避免SELECT *,只查询需要的字段
- 复杂查询使用EXPLAIN分析执行计划
-
连接池配置:
properties复制spring.datasource.druid.initial-size=5 spring.datasource.druid.max-active=20 spring.datasource.druid.max-wait=60000 spring.datasource.druid.time-between-eviction-runs-millis=60000
4.3 前端性能提升
-
资源合并压缩:
- 使用Maven插件合并CSS/JS
- 开启Gzip压缩
-
懒加载:
- 图片和音频文件按需加载
javascript复制$(window).scroll(function(){ if($(window).scrollTop() + $(window).height() > $(document).height() - 100) { loadMoreSongs(); } }); -
本地缓存:
- 用localStorage缓存用户偏好设置
- 首次加载后部分数据存sessionStorage
5. 部署与监控
5.1 生产环境部署
采用Nginx+Tomcat组合:
code复制upstream music_server {
server 127.0.0.1:8080 weight=1;
server 192.168.1.2:8080 weight=2;
}
server {
listen 80;
server_name music.example.com;
location / {
proxy_pass http://music_server;
proxy_set_header Host $host;
}
location /music/ {
alias /data/music_files/;
expires 30d;
}
}
5.2 监控方案
-
Spring Boot Actuator:
properties复制management.endpoints.web.exposure.include=health,info,metrics management.endpoint.health.show-details=always -
自定义监控指标:
java复制@RestController public class MonitorController { @Autowired private MeterRegistry registry; @GetMapping("/play") public String playSong(@RequestParam Integer songId) { registry.counter("song.play.count", "songId", songId.toString()).increment(); // ... } } -
日志收集:
- 使用Logback+ELK方案
- 关键业务操作记录详细日志
6. 踩坑与解决方案
-
音频流播放中断:
- 现象:大文件播放到一半中断
- 原因:Tomcat默认连接超时30秒
- 解决:配置server.connection-timeout=0
-
并发收藏问题:
- 现象:收藏计数偶尔不准确
- 原因:高并发下先读后写导致
- 解决:改用乐观锁更新
sql复制UPDATE song SET favorite_count = favorite_count + 1 WHERE id = ? AND favorite_count = ? -
内存泄漏:
- 现象:运行几天后OOM
- 原因:未关闭的FileInputStream
- 解决:使用try-with-resources
java复制try(InputStream is = new FileInputStream(file)) { // 处理文件 } -
跨域会话丢失:
- 现象:前后端分离部署后登录状态不保持
- 解决:配置CORS时允许credentials
java复制@Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://frontend.com") .allowCredentials(true); } }; }
这个项目从技术选型到最终上线遇到了不少挑战,但收获更大。最大的体会是:Spring Boot确实能极大提升开发效率,但在性能关键点还是需要深入理解底层原理。比如缓存策略、数据库优化等场景,自动配置并不能解决所有问题。