博客简介

主要内容:基于 web 技术实现一个有声小说项目
具体包括:项目背景、项目介绍、具体流程、实现代码、结果分析

项目实现

一、立项

项目名:AF有声小说
为了验证学习成果、印证知识点的掌握程度,模仿 “喜马拉雅FM” 实现一个有声小说项目,实现其部分功能。主要模仿用户对有声小说类软件的基础功能需求,目的是实现用户的注册、登录、对书籍的管理与对音频的操作等,基于Java语言的特性和组件开发的特点,预留了其他的功能实现接口留待日后补充,CSS样式开发尚未学习,留待日后补充。

二:需求分析

核心需求:在浏览器上实现小说的上传与音频的上传
可拓展需求:增加其他的可提升可用性的功能,如收藏、下载等;提升工作性能,比如减小音频的播放等待时间、压缩文件大小等;增强美观性,比如对声音进行柔化处理、对界面进行美化处理等
项目的边界:

核心功能:书籍、章节与音频的管理
必要辅助功能:用户管理(注册、登录、注销等)
暂不实现:功能、性能、美观等,这部分非必要功能此后继续慢慢补充

三、可行性评估

需要完成的主要功能有:
1、用户注册
2、用户登录
3、添加书籍
4、添加章节
5、录制各章节的对应音频
6、上传音频
7、音频播放

四、技术预研

如何在浏览器上实现音频的播放、如何在浏览器上实现音频的录制与上传
通过学习官网上的实例代码,模仿使用视频的录制上传
学习用record.html:

<meta charset="utf-8"><style>.button {border: solid 1px black;padding: 12px;width: 120px;height: 50px;line-height: 50px;text-align: center;}
</style><div class="left"><div id="startButton" class="button">开始录制</div><h2>预览</h2><!-- autoplay 自动播放 --><!-- muted 静音 --><video id="preview" width="160" height="120" controls muted></video>
</div><div class="right"><div id="stopButton" class="button">停止</div><h2>录制中</h2><!-- controls 显示播放器的控制面板(播放、暂停、停止)--><video id="recording" width="160" height="120" controls></video><div id="downloadButton" class="button">下载</div>
</div><script src="record.js" charset="utf-8"></script>

学习用record.js:

console.log("OK");// document 是在浏览器中运行时一直存在的一个变量,表示的意思是代表文档树
// html Document Object Model Tree    DOM 树
// document 粗糙的可以理解成这棵树的根// getElementById 从树上,根据 id,找到对应的结点(标签)
let preview = document.getElementById("preview");
let recording = document.getElementById("recording");
let startButton = document.getElementById("startButton");
let stopButton = document.getElementById("stopButton");
let downloadButton = document.getElementById("downloadButton");function wait(delayInMS) {// setTimeout(执行什么方法,多少毫秒之后)// 类似 Java 中的定时器(Timer)// 设定一个闹钟一样的效果return new Promise(resolve => setTimeout(resolve, delayInMS));
}function startRecording(stream, lengthInMS) {console.log("开始录制");let recorder = new MediaRecorder(stream);   // 定义一个媒体录制对象let data = [];// 当(on) 数据(data)可用(available) 时,执行该方法recorder.ondataavailable = function (event) {console.log("数据可用");// event.data 录制下来的视频和音频数据,存入 data 数组data.push(event.data);  // 线性表的尾插};// 开始录制recorder.start();// resolve 成功的时候应该执行的方法,对应 then 传入的方法// reject 失败的时候应该执行的方法,对应 catch 传入的方法let stopped = new Promise(function (resolve, reject) {recorder.onstop = resolve;recorder.onerror = function(event) { reject(event.name);}});// 持续 lengthInMS 时间后,执行 then 中的方法let recorded = wait(lengthInMS).then(function() {// 20 秒之后// 判断 recorder 是否还在录制,如果还在录制 == "recording",则,停止录制if (recorder.state == "recording") {console.log("停止录制");recorder.stop();}});return Promise.all([stopped,recorded]).then(() => data);
}function startCapturing() {console.log("点击采集");// 会触发,申请权限的操作let promise = navigator.mediaDevices.getUserMedia({video: true,        // 申请摄像头权限audio: true         // 申请麦克风权限});// 如果用户同意了,就执行 then 中的方法,如果失败(用户不同意 or 其他失败)会执行 catch 中的方法let promise2 = promise.then(function(stream) {console.log("同意授权");// 用户同意了// stream 变量就代表录制的视频和音频了preview.srcObject = stream;// 处理兼容性的,类似 if (!preview.captureStream) { preview.captureStream = preview.mozCaptureStream; }preview.captureStream = preview.captureStream || preview.mozCaptureStream;// 接着执行的是,当 preview 开始(on) 播放(palying) 时,执行 then 的方法// resolve 形参对应的实参就是 xxxx 函数return new Promise(function(resolve) {preview.onplaying = resolve;});});function xxxx() {return startRecording(preview.captureStream(), 5000); //}promise2.then(xxxx) // 调用 function(resolve) { ... } 这个函数.then(function (data) {console.log("使用录制下来的数据");console.log(data);let recordedBlob = new Blob(data, { type: "video/webm" });recording.src = URL.createObjectURL(recordedBlob);}).catch(e => {console.log(e);});
}function stopRecording() {console.log("点击了结束录制");
}// startButton.addEventListener("click", startRecording);   <-- 和下面的写法,目前可以认为是一样的效果
startButton.onclick = startCapturing;
// 进行事件绑定,发生了 startButton 的点击(click)事件后,
// 请执行 startRecording
// 这种形态就是俗称的回调函数(callback)
// 当 startButton 上有了 click 事件时,startButton.onclick();stopButton.onclick = stopRecording;
// 在 stopButton 发生了(on) 点击(click)事件后,执行 stopRecording 函数

实现效果:
初始页面:

点击了开始录制:

允许开启摄像头:

其余部分尝试后从控制台观察,可以实现功能无错误,预研结束

五、数据建模

需要的数据模型:用户Users、书本Books、章节Sections、音频Audios
数据关系的简易E-R图:

各模型属性:
Users:uid(用户id)、username(用户名)、password(密码)
Boods:bid(小说id)、uid(上传用户id)、title(小说名称)
Sections:sid(章节id)、bid(所属小说id)、name(章节名)
Audios:aid(音频id)、sid(对应章节id)、uuid(唯一标识码)、 type(音频类型)、content(音频内容)

六、代码实现

1、建库、建表:实现代码如下

create database AudioFiction charset utf8mb4;
use AudioFiction;
create table users (uid int primary key auto_increment comment '用户id',username varchar(64) not null unique comment '用户名',password char(64) not null comment '经过sha-256计算后的用户密码'
);
create table books (bid int primary key auto_increment comment '小说id',uid int not null comment '上传用户id',title varchar(100) not null comment '小说名称'
);
create table sections (sid int primary key auto_increment comment '章节id',bid int not null comment '属于哪本小说的id',name varchar(100) comment '章节名称'
);
create table audios (aid int primary key auto_increment comment '音频id',sid int not null unique comment '属于哪个章节的id',uuid char(36) not null comment 'uuid音频的唯一标识',type varchar(20) not null comment '音频类型audio/wmv audio/mp3',content longblob default null comment '音频内容'
);

2、准备开发环境:配置XML文件,导入依赖包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><!-- 项目基本信息 --><groupId>zjw</groupId><artifactId>AudioFiction</artifactId><version>1.0-SNAPSHOT</version><!-- war 包形式打包,IDEA会根据这个选项,自动创建 artifacts 选项--><packaging>war</packaging><!-- 项目的字符集 + 字符集选项配置 --><properties><encoding>utf-8</encoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target></properties><!-- 添加需要的第三方依赖jar包 --><dependencies><!-- 提供 Servlet 提供的标准接口 --><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><!-- 这个 jar 包只在开发 + 编译阶段使用,运行阶段不需要 --><scope>provided</scope></dependency><!-- 添加 MySQL Driver 的依赖 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version></dependency><!-- 添加处理 json 数据的依赖 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.62</version></dependency></dependencies></project>

3、需求方法列表:根据需求写出请求方法,URL
需求如下:
1、匿名用户,可以查看所有书的列表,选择出想听的书
2、已登录用户,可以上传一本书且被其他用户看到
3、书的上传者,可以录入新的章节且被其它用户看到
4、书的上传者,可以为某个章节录入音频且被其它用户看到
5、匿名用户,可以选择一本想听的书,看到书的章节列表
6、匿名用户,可以选择某一章节,播放该章节的对应音频
7、用户管理

如果只需要从服务器获取数据——GET
如果需要向服务器提供数据——POST
4、代码架构设计: MVC模式
MVC模式:Model-View-Controller,模型、视图、控制器框架。M(Model)封装应用程序的数据结构和事务逻辑,集中体现应用程序的状态,当数据改变的时候能够在视图里体现出来;V(View)是Model的外在表现,当模型改变时有所改变;C(Controller)对用户的输入进行响应,将模型和视图联系到一起,负责将数据写到模型中,并调用视图

Servlet:处理接入逻辑
Service:统一的对象处理方法,数据加工
DAO:数据访问对象,负责对数据库进行操作
MySQL:数据存储仓库,用来存储输入的数据和运行时数据
5、编写代码: 创建好各传输层包,开始按照框架开发

创建包结构如图,将Java类按结构分类管理,有助于管理和修改
这里按照各包中的类统一罗列,实际编写时按照对象编写,例如一次性处理完 User 所有的相关类,包括 Model(User)、DAO(UserDao)、Service(UserService)、Servlet(UserLoginServlet、UserRegisterServler),将 User 对象的相关类在各个包中分别编写完成再对其进行测试,确认无误就开始下一个对象的编写直到全部完成
执行逻辑图:

Model:一般来说,这类对象都要重写 toString(),equals(),hashCode()方法

//初始化User对象
package zjw.Model;import java.util.Objects;public class User {public int uid;public String username;public User() {}public User(int uid, String username) {this.uid = uid;this.username = username;}@Overridepublic String toString() {return "User{" +"uid=" + uid +", username='" + username + '\'' +'}';}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (!(o instanceof User)) return false; User user = (User) o;return uid == user.uid &&Objects.equals(username, user.username);}@Overridepublic int hashCode() {return Objects.hash(uid, username);}
}
//初始化book对象
package zjw.Model;import java.util.ArrayList;
import java.util.List;
import java.util.Objects;public class Book {public int bid;public User user;public String title;public List<Section> sections;public Book() {this.sections = new ArrayList<>();}public Book(int bid, User user, String title) {this.bid = bid;this.user = user;this.title = title;this.sections = new ArrayList<>();}@Overridepublic String toString() {return "Book{" +"bid=" + bid +", user=" + user +", title='" + title + '\'' +", sections=" + sections +'}';}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (!(o instanceof Book)) return false;Book book = (Book) o;return bid == book.bid &&Objects.equals(user, book.user) &&Objects.equals(title, book.title) &&Objects.equals(sections, book.sections);}@Overridepublic int hashCode() {return Objects.hash(bid, user, title, sections);}
}
//初始化Section对象
package zjw.Model;public class Section {public int sid;public String name;// 如果关联声音,uuid 就是保存声音的 uuid// 否则,uuid == nullpublic String uuid;public Section() {}public Section(int sid, String uuid, String name) {this.sid = sid;this.uuid = uuid;this.name = name;}@Overridepublic String toString() {return "Section{" +"uuid='" + uuid + '\'' +"name='" + name + '\'' +'}';}
}
//初始化Audio对象
package zjw.Model;import java.io.InputStream;public class Audio {public String contentType;public InputStream inputStream;public Audio(String type, InputStream content) {this.contentType = type;this.inputStream = content;}
}

Util:工具类,用来保存数据库连接

//使用单例模式,创建数据库连接
package zjw.Util;import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;public class DB {private static volatile DataSource dataSource = null;private static DataSource initDataSource() {MysqlDataSource mysqlDataSource = new MysqlDataSource();mysqlDataSource.setServerName("127.0.0.1");mysqlDataSource.setPort(3306);mysqlDataSource.setUser("数据库用户名");mysqlDataSource.setPassword("数据库密码");mysqlDataSource.setDatabaseName("audiofiction");mysqlDataSource.setCharacterEncoding("utf8");mysqlDataSource.setUseSSL(false);return mysqlDataSource;}public static Connection getConnection() throws SQLException {if (dataSource == null) {synchronized (DB.class) {if (dataSource == null) {dataSource = initDataSource();}}}return dataSource.getConnection();}
}

DAO:数据库访问层,用来传输SQL语句操作数据库,即Java实现数据库的增/删/查/改

//用户表操作
package zjw.DAO;import zjw.Model.User;
import zjw.Util.DB;import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.*;/*** Java 代码表示的 INSERT/UPDATE/DELETE/SELECT** 关于用户的密码,不要保存明文密码* 如果数据从你这里泄露了,则所有用户的密码也跟着全部泄露了* 一般都是保存做完 hash 的密码(这里选择 sha-256 这个hash算法)*/
public class UserDao {public User insert(String username, String plainPassword) throws SQLException {String password = encrypt(plainPassword);String sql = "insert into users (username, password) values (?, ?)";try (Connection c = DB.getConnection()) {try (PreparedStatement s = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {s.setString(1, username);s.setString(2, password);s.executeUpdate();try (ResultSet r = s.getGeneratedKeys()) {if (!r.next()) {return null;}return new User(r.getInt(1), username);}}}}public User select(String username, String plainPassword) throws SQLException {String password = encrypt(plainPassword);String sql = "select uid from users where username = ? and password = ?";try (Connection c = DB.getConnection()) {try (PreparedStatement s = c.prepareStatement(sql)) {s.setString(1, username);s.setString(2, password);try (ResultSet r = s.executeQuery()) {if (!r.next()) {return null;}return new User(r.getInt(1), username);}}}}// 这个做法实际上也不适合生产环境真正使用// 但至少比明文的情况要安全一点private String encrypt(String plain) {try {MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");byte[] bytes = plain.getBytes();byte[] digest = messageDigest.digest(bytes);StringBuilder sb = new StringBuilder();for (byte b : digest) {sb.append(String.format("%02x", b));}return sb.toString();} catch (NoSuchAlgorithmException e) {throw new RuntimeException(e);}}public static void main(String[] args) {String a = "123";UserDao userDao = new UserDao();String encrypt = userDao.encrypt(a);System.out.println(encrypt);}
}
//书籍表操作
package zjw.DAO;import zjw.Model.Book;
import zjw.Model.User;
import zjw.Util.DB;import java.sql.*;
import java.util.ArrayList;
import java.util.List;public class BookDao {public Book insert(User user, String title) throws SQLException {String sql = "insert into books (uid, title) values (?, ?)";try (Connection c = DB.getConnection()) {try (PreparedStatement s = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {s.setInt(1, user.uid);s.setString(2, title);s.executeUpdate();try (ResultSet r = s.getGeneratedKeys()) {if (!r.next()) {return null;}return new Book(r.getInt(1), user, title);}}}}public List<Book> selectAll() throws SQLException {// 联表查询String sql = "select bid, title, users.uid, users.username " +"from books, users " +"where books.uid = users.uid " +"order by bid desc";List<Book> books = new ArrayList<>();try (Connection c = DB.getConnection()) {try (PreparedStatement s = c.prepareStatement(sql)) {try (ResultSet r = s.executeQuery()) {while (r.next()) {User user = new User(r.getInt("uid"), r.getString("username"));Book book = new Book(r.getInt("bid"), user, r.getString("title"));books.add(book);}}}}return books;}public Book selectByBid(int bid) throws SQLException {String sql = "select bid, title, users.uid, users.username " +"from books, users " +"where books.uid = users.uid and bid = ?";try (Connection c = DB.getConnection()) {try (PreparedStatement s = c.prepareStatement(sql)) {s.setInt(1, bid);try (ResultSet r = s.executeQuery()) {if (!r.next()) {return null;}User user = new User(r.getInt("uid"), r.getString("username"));return new Book(r.getInt("bid"), user, r.getString("title"));}}}}
}
//章节表操作
package zjw.DAO;import zjw.Model.Book;
import zjw.Model.Section;
import zjw.Util.DB;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;public class SectionDao {public void insert(int bid, String name) throws SQLException {String sql = "insert into sections (bid, name) values (?, ?)";try (Connection c = DB.getConnection()) {try (PreparedStatement s = c.prepareStatement(sql)) {s.setInt(1, bid);s.setString(2, name);s.executeUpdate();}}}public List<Section> selectByBid(int bid) throws SQLException {String sql = "select sections.sid, uuid, name " +"from sections left join audios on sections.sid = audios.sid " +"where bid = ? order by sections.sid";List<Section> sections = new ArrayList<>();try (Connection c = DB.getConnection()) {try (PreparedStatement s = c.prepareStatement(sql)) {s.setInt(1, bid);try (ResultSet r = s.executeQuery()) {while (r.next()) {Section section = new Section(r.getInt("sid"),r.getString("uuid"),r.getString("name"));sections.add(section);}}}}return sections;}
}
//音频表操作
package zjw.DAO;import zjw.Model.Audio;
import zjw.Util.DB;import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class AudioDao {public void insert(int sid, String uuid, String contentType, InputStream inputStream) throws SQLException {String sql = "insert into audios (sid, uuid, type, content) values (?, ?, ?, ?)";try (Connection c = DB.getConnection()) {try (PreparedStatement s = c.prepareStatement(sql)) {s.setInt(1, sid);s.setString(2, uuid);s.setString(3, contentType);s.setBlob(4, inputStream);s.executeUpdate();}}}public Audio select(String uuid) throws SQLException {String sql = "select type, content from audios where uuid = ?";try (Connection c = DB.getConnection()) {try (PreparedStatement s = c.prepareStatement(sql)) {s.setString(1, uuid);try (ResultSet r = s.executeQuery()) {if (!r.next()) {return null;}return new Audio(r.getString("type"), r.getBinaryStream("content"));}}}}
}

注:sha-256是一种hash算法,可以达到对密码的加密效果,一定程度上保证用户账号的安全性,将加密后的字符进行解密就可以得到原密码,但是在工作场景中一般也不建议使用这种做法,此处仅作示例

Service:封装对数据的操作处理

//User
package zjw.Service;import zjw.DAO.UserDao;
import zjw.Model.User;import java.sql.SQLException;/*
提供偏向业务角度的方法*/
public class UserService {private UserDao userDao;public UserService() {userDao = new UserDao();}public User register(String username, String password) throws SQLException {return userDao.insert(username, password);}public User login(String username, String password) throws SQLException {return userDao.select(username, password);}
}
package zjw.Service;import zjw.DAO.BookDao;
import zjw.DAO.SectionDao;
import zjw.Model.Book;
import zjw.Model.Section;
import zjw.Model.User;import java.sql.SQLException;
import java.util.List;public class BookService {private BookDao bookDao;private SectionDao sectionDao;public BookService() {bookDao = new BookDao();sectionDao = new SectionDao();}public List<Book> list() throws SQLException {return bookDao.selectAll();}public Book post(String title, User user) throws SQLException {return bookDao.insert(user, title);}public Book get(int bid) throws SQLException {Book book = bookDao.selectByBid(bid);if (book == null) {return null;}book.sections = sectionDao.selectByBid(bid);return book;}public void addSection(int bid, String name) throws SQLException {sectionDao.insert(bid, name);}
}
package zjw.Service;import zjw.DAO.AudioDao;
import zjw.Model.Audio;import javax.servlet.http.Part;
import java.io.IOException;
import java.sql.SQLException;
import java.util.UUID;public class AudioService {private AudioDao audioDao;public AudioService() {audioDao = new AudioDao();}public String save(int sid, Part audio) throws IOException, SQLException {String uuid = UUID.randomUUID().toString();audioDao.insert(sid, uuid, audio.getContentType(), audio.getInputStream());return uuid;}public Audio get(String uuid) throws SQLException {return audioDao.select(uuid);}
}

Servlet:

package zjw.Servlet;import zjw.Model.User;
import zjw.Service.UserService;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.sql.SQLException;@WebServlet("/register")
public class UserRegisterServlet extends HttpServlet {private UserService userService;@Overridepublic void init() throws ServletException {// Servlet 生命周期的内容userService = new UserService();}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {// 每次都带上 utf-8 的设置,字符集不会出问题req.setCharacterEncoding("utf-8");String username = req.getParameter("username");String password = req.getParameter("password");// 没有做参数正确性检查try {User user = userService.register(username, password);if (user == null) {// 没有注册成功// 没有做太多易用性考虑:没有告诉用户错误原因resp.sendRedirect("/register.html");return;}// 把当前用户种入 session 中,下次资源访问时携带的登陆用户信息HttpSession session = req.getSession();session.setAttribute("user", user);// 跳转回首页resp.sendRedirect("/");} catch (SQLException e) {throw new ServletException(e);}}
}
package zjw.Servlet;import zjw.Model.User;
import zjw.Service.UserService;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.sql.SQLException;@WebServlet("/login")
public class UserLoginServlet extends HttpServlet {private UserService userService;@Overridepublic void init() throws ServletException {userService = new UserService();}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {req.setCharacterEncoding("utf-8");String username = req.getParameter("username");String password = req.getParameter("password");// 没有做参数正确性检查try {User user = userService.login(username, password);if (user == null) {// 没有注册成功// 没有做太多易用性考虑:没有告诉用户错误原因resp.sendRedirect("/login.html");return;}// 把当前用户种入 session 中,下次资源访问时携带的登陆用户信息HttpSession session = req.getSession();session.setAttribute("user", user);// 跳转回首页resp.sendRedirect("/");} catch (SQLException e) {throw new ServletException(e);}}
}
package zjw.Servlet;import zjw.Model.Book;
import zjw.Model.User;
import zjw.Service.BookService;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.sql.SQLException;@WebServlet("/post-book")
public class BookPostServlet extends HttpServlet {private BookService bookService;@Overridepublic void init() throws ServletException {bookService = new BookService();}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {req.setCharacterEncoding("utf-8");String title = req.getParameter("title");HttpSession session = req.getSession();User user = (User) session.getAttribute("user");if (user == null) {resp.sendRedirect("/login.html");return;}try {Book book = bookService.post(title, user);if (book != null) {resp.sendRedirect("/book.jsp?bid=" + book.bid);} else {resp.sendRedirect("/add-book.jsp");}} catch (SQLException e) {throw new ServletException(e);}}
}
package zjw.Servlet;import zjw.Service.BookService;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.SQLException;@WebServlet("/post-section")
public class SectionPostServlet extends HttpServlet {private BookService bookService;@Overridepublic void init() throws ServletException {bookService = new BookService();}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {req.setCharacterEncoding("utf-8");int bid = Integer.parseInt(req.getParameter("bid"));String name = req.getParameter("name");try {bookService.addSection(bid, name);} catch (SQLException e) {throw new ServletException(e);}resp.sendRedirect("/book.jsp?bid=" + bid);}
}
package zjw.Servlet;import zjw.Service.AudioService;
import zjw.Service.BookService;import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;@MultipartConfig
@WebServlet("/upload/audio")
public class AudioUploadServlet extends HttpServlet {private AudioService audioService;@Overridepublic void init() throws ServletException {audioService = new AudioService();}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {req.setCharacterEncoding("utf-8");String s = req.getParameter("sid");// s = nullint sid = Integer.parseInt(s);Part audio = req.getPart("audio");// 通过这个输入流,就可以读取到声音的所有数据// InputStream inputStream = audio.getInputStream();// 1. 保存声音,得到声音的 uuid,同时关联 sidresp.setContentType("utf-8");resp.setContentType("application/json");PrintWriter writer = resp.getWriter();try {String uuid = audioService.save(sid, audio);writer.printf("{\"uuid\": \"%s\"}%n", uuid);} catch (SQLException e) {e.printStackTrace();resp.setStatus(500);writer.printf("{\"reason\": \"%s\"}%n", e.getMessage());}}
}
package zjw.Servlet;import zjw.Model.Audio;
import zjw.Service.AudioService;import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.SQLException;@WebServlet("/audio/get")
public class AudioGetServlet extends HttpServlet {private AudioService audioService;@Overridepublic void init() throws ServletException {audioService = new AudioService();}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {req.setCharacterEncoding("utf-8");String uuid = req.getParameter("uuid");Audio audio = null;try {audio = audioService.get(uuid);if (audio == null) {resp.sendError(404, "没有这段声音");return;}} catch (SQLException e) {throw new ServletException(e);}resp.setContentType(audio.contentType);ServletOutputStream outputStream = resp.getOutputStream();byte[] buf = new byte[1024];int len;while ((len = audio.inputStream.read(buf)) != -1) {outputStream.write(buf, 0, len);}audio.inputStream.close();}
}

6、前端代码: 简单实现前端显示和跳转的页面

创建包结构如图,
注册页面: register.html

<!DOCTYPE html>
<html lang="zh-hans">
<head><meta charset="UTF-8"><title>AudioFiction | 用户注册</title>
</head>
<body><form method="post" action="register"><div><label for="username">用户名:<input type="text" id="username" name="username" /></label></div><div><label for="password">密码:<input type="password" id="password" name="password" /></label></div><div><button type="submit">注册</button></div></form>
</body>
</html>

登陆页面: login.html

<!DOCTYPE html>
<html lang="zh-hans">
<head><meta charset="UTF-8"><title>AudioFiction | 用户登陆</title>
</head>
<body><form method="post" action="login"><div><label for="username">用户名:<input type="text" id="username" name="username" /></label></div><div><label for="password">密码:<input type="password" id="password" name="password" /></label></div><div><button type="submit">登陆</button></div></form>
</body>
</html>

首页 index.jsp

<%@ page import="zjw.Service.BookService" %>
<%@ page import="java.util.List" %>
<%@ page import="zjw.Model.Book" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!// 这里定义属性方法private BookService bookService;public void jspInit() {bookService = new BookService();}
%>
<%// 这里是执行 java 代码List<Book> books = bookService.list();
%>
<!DOCTYPE html>
<html lang="zh-hans">
<head><meta charset="utf-8"><title>听书FM</title>
</head>
<body><header><nav><ol><li><a href="/register.html">注册</a></li><li><a href="/login.html">登陆</a></li><li><a href="/add-book.jsp">上传书籍</a></li></ol></nav></header><main><!--<ol><li><a href="/book?bid=<bid>">书籍名称</a> 上传者: 用户名</li></ol>--><ol><% for (Book book : books) { %><li><a href="/book.jsp?bid=<%= book.bid %>"><%= book.title %></a><span>上传者: <%= book.user.username %></span></li><% } %></ol></main>
</body>
</html>

因为需要动态判断用户是否登录,未登录要跳转登陆页面,所以用 .JSP 文件实现动态资源
add-book.jsp

<%@ page import="zjw.Model.User" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%User user = (User) session.getAttribute("user");if (user == null) {response.sendRedirect("/login.html");return;}
%>
<html>
<head><title>听书FM | 上传书籍</title>
</head>
<body><form method="post" action="/post-book"><div><label for="title">书籍标题:<input type="text" id="title" name="title"></label></div><div><button type="submit">上传</button></div></form>
</body>
</html>

book.jsp

<%@ page import="zjw.Service.BookService" %>
<%@ page import="zjw.Model.Book" %>
<%@ page import="zjw.Model.User" %>
<%@ page import="zjw.Model.Section" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!private BookService bookService;public void jspInit() {bookService = new BookService();}
%>
<%// 根据 URL 中的 bid,获取书籍信息int bid = Integer.parseInt(request.getParameter("bid"));Book book = bookService.get(bid);if (book == null) {response.sendError(404, "没有这本书");return;}// 获取当前登陆用户信息User user = (User) session.getAttribute("user");
%>
<html>
<head><meta charset="utf-8"><title>听书FM | 书籍详情</title>
</head>
<body><h1><%= book.title %></h1><p>上传者: <%= book.user.username %></p><!-- 如果当前用户就是书籍的上传用户,允许用户进行添加新章节 --><% if (user != null && book.user.equals(user)) { %><h2>添加新章节</h2><form method="post" action="post-section"><input type="hidden" name="bid" value="<%= book.bid %>"><div><label for="name">章节名称:<input type="text" id="name" name="name"></label></div><div><button type="submit">提交</button></div></form><% } %><h2>章节列表</h2><ol><% for (Section section : book.sections) { %><% if (section.uuid != null) { %><li><%= section.name %><audio controls src="/audio/get?uuid=<%= section.uuid %>"></audio></li><% } else if (user != null && book.user.equals(user)) { %><li><%= section.name %><a href="record.jsp?sid=<%= section.sid %>">录制声音</a></li><% } %><% } %></ol>
</body>
</html>

音频录制部分:进行了简单了CSS处理,用 JS 和 JSP 结合实现功能
record.js

"use strict";// 日志显示功能
let logElement = document.getElementById("log");
function log(message) {logElement.innerHTML += (message + "\n");
}// 定义全局变量(java 中就没有全局变量,js 中有全局变量),功能类似于 java 中的静态变量、属性
let captureStream;      // 用来 保存采集的 stream
let mediaRecorder;      // 用来 保存录制器
let data = [];          // 用来 保存录好的数据
let audioBlob;          // 用来 保存转成 Blob 类型的录好的数据// 从 html 中获取标签
let startButton = document.getElementById("startButton");
let recordButton = document.getElementById("recordButton");
let stopButton = document.getElementById("stopButton");
let submitButton = document.getElementById("submitButton");
let preview = document.getElementById("preview");// 为开始采集按钮绑定 点击(click)事件的动作
startButton.addEventListener("click", function () {log("点击了开始采集按钮 -> 会弹出授权请求");let promise = navigator.mediaDevices.getUserMedia({audio: true         // 只有 audio,没有 video});promise.then(function (stream) {log("用户同意了授权 -> 记录了采集数据");captureStream = stream;     // 保存 stream 到全局变量 captureStream 中});
});// 为开始录制按钮绑定 点击(click)事件的动作
recordButton.addEventListener("click", function () {log("点击了开始录制按钮 -> 开始录制,每 3 秒收集一次数据");if (!captureStream) {log("错误:必须先点击开始采集按钮");return;}// mediaRecorder 也是全局变量mediaRecorder = new MediaRecorder(captureStream);mediaRecorder.ondataavailable = function (event) {log("录制数据可用事件 -> 保存数据");data.push(event.data);};mediaRecorder.start(3000);
});// 为停止录制按钮绑定 点击(click)事件的动作
stopButton.addEventListener("click", function () {log("点击了停止录制按钮 -> 停止录制");if (!mediaRecorder) {log("错误:必须先点击开始录制按钮");return;}mediaRecorder.onstop = function () {log("录制停止事件 -> 准备预览功能的数据流");audioBlob = new Blob(data, {type: "audio/webm"      // 类型是 audio/webm});preview.src = URL.createObjectURL(audioBlob);};mediaRecorder.stop();
});// 为上传按钮绑定 点击(click)事件的动作
submitButton.addEventListener("click", function () {log("点击了上传按钮 -> 通过 form 表单,向服务器提交录制下来的数据");if (!audioBlob) {log("错误:必须先点击停止录制按钮");return;}let xhr = new XMLHttpRequest();xhr.open("post", "/upload/audio?sid=" + sid);xhr.onload = function () {log("服务器应答事件 -> 打印应答信息");log(xhr.status);log(xhr.responseText);};let formData = new FormData();formData.set("audio", audioBlob);xhr.send(formData);
});

record.jsp

<%@ page import="zjw.Model.Section" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><meta charset="utf-8"><title>听书FM | 录制声音</title><link rel="stylesheet" href="/css/record.css">
</head>
<body><div><div id="startButton" class="button">开始采集</div><div id="recordButton" class="button">开始录制</div><div id="stopButton" class="button">停止录制</div><div id="submitButton" class="button">上传</div><h2>试听</h2><audio id="preview" controls></audio></div><div><pre id="log"></pre></div><script charset="utf-8">let sid = <%= request.getParameter("sid") %>;</script><script charset="utf-8" src="/js/record.js"></script>
</body>
</html>
.button {display: inline-block;width: 120px;height: 50px;line-height: 50px;text-align: center;border: 1px solid black;border-radius: 6px;box-shadow: black 0 2px 0;background-color: brown;cursor: pointer;
}

七、功能测试

运行截图:
匿名登陆状态:
可以查看已有书籍列表

可以选择已有书籍进行播放,但不可以上传新章节

注册:
可以进行账号的注册,当注册用户名页面会自动跳转回首页

当用户名重复时,会弹出500错误,此处并没有错误处理

登录:
登陆完成跳转回首页,登陆失败则不跳转可以继续输入正确的用户信息

登陆后查看自己上传的书籍,会有上传章节窗口

上传书籍:
需要先上传书籍名

输入书籍名称后会跳转添加章节页面

提交章节名后会显示章节列表,且提供音频录制按钮

点击声音录制后会跳转到声音录制界面

当未点击开始采集直接执行录制或者其他功能时,逻辑错误,会打印提示信息

按照顺序采集,录制,停止后,音频可以正常播放

点击上传之后会将音频上传并且打印音频的 uuid

此时首页就会显示已上传书籍,可以供匿名用和登录用户进行播放

简单测试:
项目功能点:登录、注册、提交书记、提交章节、音频测试
测试核心功能点:音频录制(目前对测试没有过系统性的学习,只简单进行部分功能测试)
逻辑功能测试:
1、直接点击开始录制按钮,不经过采集授权:结果符合预期

2、直接点击停止录制按钮,不经过开始录制:结果符合预期

3、未录制情况下直接上传:结果符合预期

4、录制音频播放测试能否听到声音:结果符合预期

详细测试:
分别录制上传时长为12s、18s、24s、30s、36s、42s的音频,对音频内容及音频时长进行测试:基本判断音频时长与质量无基本问题,结果符合预期

八、项目部署

部署流程:
1、把代码中数据的登陆用户名/密码/库改成 linux 的
2、在 linux 的数据库上建库、建表
3、项目打包成 .war 格式
4、把打好的包放在 linux 的tomcao 下
5、测试
目前个人云服务端有一些小问题,这部分日后实现(其实是我忘记了数据库密码)

九、概述

目前这个项目只实现了1.0版本,界面还比较粗糙,还可以改进的有:
1、使用CSS层叠样式表美观化浏览器界面;
2、功能性还需要拓展,主要集中在用户管理和书籍管理部分,可以增加收藏、删除等更多功能
3、增设管理员对象,实现对用户对象和书籍对象的进一步管理,增强权限分级
4、可以对音频加以处理,使声音更加柔和
5、增加个性化设计,提供给用户可以主动选择的个性化选项,增强用户舒适度
6、进一步细化测试
7、…
这个项目有许多可以进一步优化的地方,功能也可以再大幅增强,目前只实现基础版框架,主要架构设计已经完成,基于语言的特性,以后可以进一步细化各种功能,尽可能打造一个完整的项目,作为自己学习的巩固和学习成果的检验。

JAVA:AudioFiction(有声小说)项目实现相关推荐

  1. java有声小说如何开发_怎么才能做有声小说播音?有声书主播如何训练?

    想要做有声小说主播要达到练好吐字用声.训练自个的语言表达技巧.提升快速准备稿件的基本功.有自个的播音性格,详细介绍一下如下: 1.练好吐字用声: 第一步就是说吐字清晰.声音圆润.普通话标准.此外,须要 ...

  2. java有声小说如何开发_怎么加入有声小说配音?如何做一个有声小说配音员

    当今世界1种东西便会有千种形状,当然了1个嗓音也会出现千种类别如今在日常生活中非常普遍的类别就会有许多,比如说娃娃音.萝莉音.大叔音及其御姐音等等这些,并且还有不同的额读法. 举个事例,如果有合适御姐 ...

  3. java有声小说如何开发_怎么才能做有声小说播音,有声书主播如何训练

    想要做有声小说主播要达到练好吐字用声.训练自个的语言表达技巧.提升快速准备稿件的基本功.有自个的播音性格,详细介绍一下如下: 1.练好吐字用声: 第一步就是说吐字清晰.声音圆润.普通话标准.此外,须要 ...

  4. 基于JAVA中文网络小说平台系统计算机毕业设计源码+系统+数据库+lw文档+部署

    基于JAVA中文网络小说平台系统计算机毕业设计源码+系统+数据库+lw文档+部署 基于JAVA中文网络小说平台系统计算机毕业设计源码+系统+数据库+lw文档+部署 本源码技术栈: 项目架构:B/S架构 ...

  5. java计算机毕业设计小说阅读网站系统源码+lw文档+系统+数据库

    java计算机毕业设计小说阅读网站系统源码+lw文档+系统+数据库 java计算机毕业设计小说阅读网站系统源码+lw文档+系统+数据库 本源码技术栈: 项目架构:B/S架构 开发语言:Java语言 开 ...

  6. 计算机毕业设计Java中文网络小说平台系统(源码+系统+mysql数据库+lw文档)

    计算机毕业设计Java中文网络小说平台系统(源码+系统+mysql数据库+lw文档) 计算机毕业设计Java中文网络小说平台系统(源码+系统+mysql数据库+lw文档) 本源码技术栈: 项目架构:B ...

  7. java计算机毕业设计小说阅读网站源码+系统+数据库+lw文档+mybatis+运行部署

    java计算机毕业设计小说阅读网站源码+系统+数据库+lw文档+mybatis+运行部署 java计算机毕业设计小说阅读网站源码+系统+数据库+lw文档+mybatis+运行部署 本源码技术栈: 项目 ...

  8. Python多线程下载有声小说

    有经验的老鸟都(未婚的)会在公司附近租房,免受舟车劳顿之苦的同时节约了大把时间:也有些人出于某种原因需要每天披星戴月地游走于公司与家之间,很不幸俺就是这其中一员.由于家和公司离得比较远,我平时在公交车 ...

  9. python爬取有声小说网站实现自动下载实例

    最近想下载一些有声小说,但是苦于没有找到批量下载,每次都是单集单集的下载的,觉得很麻烦,就考虑用python写一个爬虫来实现自动搜集小说,自动下载.下面就是开始展开漫漫的爬虫之路. 基础的就不多说了, ...

最新文章

  1. Thread类源码剖析
  2. 手机没电了 鸿蒙还有用吗,记住这几个技巧,手机没电时可以有效延长使用时间...
  3. iText创建一个含有中文的pdf文档
  4. [置顶] Java Socket实战之一 单线程通信
  5. k-Means——经典聚类算法实验(Matlab实现)
  6. fastai学习笔记——安装
  7. python关闭对象语法_Python基础及语法(七)
  8. 在线验证json字符串
  9. pandas系列 read_csv 与 to_csv 方法各参数详解(全,中文版)
  10. Nginx + Tomcat6配置负载均衡
  11. SimpleDateFormat 格式化日期
  12. 卖计算机英语对话,买电脑英语情景对话
  13. PUN 2 菜鸟养成记 3进入游戏
  14. Chrome浏览器地址栏自动填充
  15. MATLAB——KNN分类器实例
  16. LeetCode-2206. 将数组划分成相等数对_Python
  17. hana odata batch
  18. xp系统蓝屏,xp系统蓝屏的详细解决过程
  19. 如何使用BSA方法进行遗传定位(水稻篇)
  20. mac工具:微信双开,一步到位

热门文章

  1. 【DataBase】数据库连接池
  2. (附源码)spring boot网上商品定制系统 毕业设计180915
  3. 计算机专业助我成长作文600,资助助我成长作文
  4. 当你在浏览器上,指尖轻轻输入 www.taobao.com 以后发生了什么?
  5. 华为datacom和RS对比有什么区别?
  6. SpringBoot -- 服务网关APIGateway
  7. c# 调用c++ lib静态库
  8. c语言 switch案例,C语言switch语句实例
  9. 脱壳,反编译 ,汇编工具集合
  10. Tushare股票分析【四】-- 通过股票代码获取股票名称