JavaWeb阶段性项目2:QQZone项目梳理
前置知识
前置准备
知识准备
已掌握JavaSE/MySQL/JDBC+HTML/CSS/JavaScript基础
并已完成了Javaweb前置知识的学习
04-JavaScript基础应用-鼠标悬浮/离开表格格式变化
05-JavaWeb-Tomcat8安装、Servlet初识
06-JavaWeb-Servlet方法/生命周期、HTTP/会话session
09-JavaWeb-阶段性项目1:最简单的后台库存管理系统
10-JavaWeb阶段性项目1:系统的servlet优化1
11-JavaWeb阶段性项目1:系统的servlet优化2
12-JavaWeb阶段性项目1:系统的servlet优化3
13-JavaWeb阶段性项目1:系统的servlet优化4
14-JavaWeb阶段性项目1:系统的servlet优化5
15-JavaWeb阶段性项目1:Servlet-api、mvc-service引入、IOC和DI
资源准备
教学资源
https://pan.baidu.com/s/1TS7QJ_a2vHHmXkggAs8RMQ
提取码:yyds
本文已经完成了视频P55前的所有内容,接下来是一个练手项目,可以很好的巩固之前视频的知识,同时更接近真实项目开发,从理解需求开始,进行一个简单Javaweb项目的设计与实现——尚硅谷2022Javaweb视频教程P56-QQZone项目
以下主要是实现过程及思路的整理,不过多涉及代码如何实现。
1 需求分析
视频中需求
首先熟悉需求
1) 用户登录
2) 登录成功,显示主界面。左侧显示好友列表;上端显示欢迎词。如果不是自己的空间,显示超链接:返回自己的空间;下端显示日志列表
3) 查看日志详情:
- 日志本身的信息(作者头像、昵称、日志标题、日志内容、日志的日期)
- 回复列表(回复者的头像、昵称、回复内容、回复日期)
- 主人回复信息
4) 删除日志
5) 删除特定回复
6) 删除特定主人回复
7) 添加日志、添加回复、添加主人回复
8) 点击左侧好友链接,进入好友的空间
需求分析需要对所进行项目的相关实际业务非常了解,确保深入理解了用户端的业务逻辑、表单结构、管理规范等。
用原型图的方式展示给用户以了解和挖掘用户的真实需求,重点是确认需求不会再有更改(如果是外包可以通过签订合同,如果是自研可以走OA流程最终确定)
2 数据库设计
2.1 概念结构设计
1) 抽取实体 : 用户登录信息、用户详情信息 、 日志 、 回贴 、 主人回复
2) 分析其中的属性:
- 用户登录信息:账号、密码、头像、昵称
- 用户详情信息:真实姓名、星座、血型、邮箱、手机号.....
- 日志:标题、内容、日期、作者
- 回复:内容、日期、作者、日志
- 主人回复:内容、日期、作者、回复
3) 分析实体之间的关系
- 用户登录信息 : 用户详情信息 1:1 PK
- 用户 : 日志 1:N
- 日志 : 回复 1:N
- 回复 : 主人回复 1:1 UK
- 用户 : 好友 M : N
按照规范设计,我们将数据库的设计过程分为六个阶段:
-
系统需求分析阶段;
-
概念结构设计阶段;
-
逻辑结构设计阶段;
-
物理结构设计阶段;
-
数据库实施阶段;
-
数据库运行与维护阶段;
这里只进行了业务级别的概念结构设计,找到了实体,分析了实体的属性,建立了实体与实体之间的联系,也就是E-R模型图的内容。
对于实体的考虑
视频中老师抽取实体时不是完全描述现实世界的方式,而是根据实际业务来设计,比如将用户分为了用户登录信息、用户详情信息两个实体
对于属性的考虑
需要平衡数据库的性能和规范度
选择空间还是时间?要根据实际业务来,查询频次低的选择空间、提高规范度,频次高的选择时间,反范式化。
2.2 逻辑结构设计
创建表的SQL代码
CREATE` `DATABASE` `qqzonedb ``CHAR` `SET` `utf8;
USE qqzonedb;
CREATE` `TABLE` ``t_user_basic` (
```id` ``INT``(11) ``NOT` `NULL` `AUTO_INCREMENT,
```loginId` ``VARCHAR``(20) ``NOT` `NULL``,
```nickName` ``VARCHAR``(50) ``NOT` `NULL``,
```pwd` ``VARCHAR``(20) ``NOT` `NULL``,
```headImg` ``VARCHAR``(20) ``DEFAULT` `NULL``,
``PRIMARY` `KEY` `(`id`),
``UNIQUE` `KEY` ``loginId` (`loginId`)
) ENGINE=INNODB AUTO_INCREMENT=6 ``DEFAULT` `CHARSET=utf8;
INSERT` `INTO` ``t_user_basic`(`id`,`loginId`,`nickName`,`pwd`,`headImg`)
VALUES` `(1,``'u001'``,``'jim'``,``'ok'``,``'h1.jpeg'``),
(2,``'u002'``,``'tom'``,``'ok'``,``'h2.jpeg'``),
(3,``'u003'``,``'kate'``,``'ok'``,``'h3.jpeg'``),
(4,``'u004'``,``'lucy'``,``'ok'``,``'h4.jpeg'``),
(5,``'u005'``,``'张三丰'``,``'ok'``,``'h5.jpeg'``);
CREATE` `TABLE` ``t_user_detail` (
```id` ``INT``(11) ``NOT` `NULL``,
```realName` ``VARCHAR``(20) ``DEFAULT` `NULL``,
```tel` ``VARCHAR``(11) ``DEFAULT` `NULL``,
```email` ``VARCHAR``(30) ``DEFAULT` `NULL``,
```birth` DATETIME ``DEFAULT` `NULL``,
```star` ``VARCHAR``(10) ``DEFAULT` `NULL``,
``PRIMARY` `KEY` `(`id`),
``CONSTRAINT` ``FK_detail_basic` ``FOREIGN` `KEY` `(`id`) ``REFERENCES` ``t_user_basic` (`id`)
) ENGINE=INNODB ``DEFAULT` `CHARSET=utf8;
CREATE` `TABLE` ``t_friend` (
```id` ``INT``(11) ``NOT` `NULL` `AUTO_INCREMENT,
```uid` ``INT``(11) ``DEFAULT` `NULL``,
```fid` ``INT``(11) ``DEFAULT` `NULL``,
``PRIMARY` `KEY` `(`id`),
``KEY` ``FK_friend_basic_uid` (`uid`),
``KEY` ``FK_friend_basic_fid` (`fid`),
``CONSTRAINT` ``FK_friend_basic_fid` ``FOREIGN` `KEY` `(`fid`) ``REFERENCES` ``t_user_basic` (`id`),
``CONSTRAINT` ``FK_friend_basic_uid` ``FOREIGN` `KEY` `(`uid`) ``REFERENCES` ``t_user_basic` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=11 ``DEFAULT` `CHARSET=utf8;
INSERT` `INTO` ``t_friend`(`id`,`uid`,`fid`)
VALUES` `(1,1,2),(2,1,3),(3,1,4),(4,1,5),(5,2,3),(6,2,1),(7,2,4),(8,3,1),(9,3,2),(10,5,1);
CREATE` `TABLE` ``t_topic` (
```id` ``INT``(11) ``NOT` `NULL` `AUTO_INCREMENT,
```title` ``VARCHAR``(100) ``NOT` `NULL``,
```content` ``VARCHAR``(500) ``NOT` `NULL``,
```topicDate` DATETIME ``NOT` `NULL``,
```author` ``INT``(11) ``NOT` `NULL``,
``PRIMARY` `KEY` `(`id`),
``KEY` ``FK_topic_basic` (`author`),
``CONSTRAINT` ``FK_topic_basic` ``FOREIGN` `KEY` `(`author`) ``REFERENCES` ``t_user_basic` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=9 ``DEFAULT` `CHARSET=utf8;
INSERT` `INTO` ``t_topic`(`id`,`title`,`content`,`topicDate`,`author`)
VALUES` `(3,``'我的空间开通了,先做自我介绍!'``,``'大家好,我是铁锤妹妹!'``,``'2021-06-18 11:25:30'``,2),(8,``'我的空间'``,``'我的空间'``,``'2021-07-14 16:16:40'``,1);
CREATE` `TABLE` ``t_reply` (
```id` ``INT``(11) ``NOT` `NULL` `AUTO_INCREMENT,
```content` ``VARCHAR``(500) ``NOT` `NULL``,
```replyDate` DATETIME ``NOT` `NULL``,
```author` ``INT``(11) ``NOT` `NULL``,
```topic` ``INT``(11) ``NOT` `NULL``,
``PRIMARY` `KEY` `(`id`),
``KEY` ``FK_reply_basic` (`author`),
``KEY` ``FK_reply_topic` (`topic`),
``CONSTRAINT` ``FK_reply_basic` ``FOREIGN` `KEY` `(`author`) ``REFERENCES` ``t_user_basic` (`id`),
``CONSTRAINT` ``FK_reply_topic` ``FOREIGN` `KEY` `(`topic`) ``REFERENCES` ``t_topic` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=6 ``DEFAULT` `CHARSET=utf8;
INSERT` `INTO` ``t_reply`(`id`,`content`,`replyDate`,`author`,`topic`)
VALUES` `(3,``'回复'``,``'2021-07-14 16:16:54'``,2,8),
(4,``'回复2222'``,``'2021-07-14 16:17:11'``,3,8),
(5,``'这里是第三个回复'``,``'2021-07-14 16:30:49'``,1,8);
CREATE` `TABLE` ``t_host_reply` (
```id` ``INT``(11) ``NOT` `NULL` `AUTO_INCREMENT,
```content` ``VARCHAR``(500) ``NOT` `NULL``,
```hostReplyDate` DATETIME ``NOT` `NULL``,
```author` ``INT``(11) ``NOT` `NULL``,
```reply` ``INT``(11) ``NOT` `NULL``,
``PRIMARY` `KEY` `(`id`),
``KEY` ``FK_host_basic` (`author`),
``KEY` ``FK_host_reply` (`reply`),
``CONSTRAINT` ``FK_host_basic` ``FOREIGN` `KEY` `(`author`) ``REFERENCES` ``t_user_basic` (`id`),
``CONSTRAINT` ``FK_host_reply` ``FOREIGN` `KEY` `(`reply`) ``REFERENCES` ``t_reply` (`id`)
) ENGINE=INNODB ``DEFAULT` `CHARSET=utf8;
2.3 物理结构设计
3 系统代码实现
3.1 pojo、DAO、service
POJO(Plain Ordinary Java Object)简单的Java对象,实际就是普通JavaBeans
pojo
将数据库的表转化为pojo
可以通过IDEA将表结构直接身成为是实体类
DAO
数据访问对象接口(Data Access Object Interface)
创建DAO接口的实现类,继承BaseDAO
BaseDAO
package com.atguigu.myssm.basedao;
import java.lang.reflect.*;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public abstract class BaseDAO<T> {
protected Connection conn ;
protected PreparedStatement psmt ;
protected ResultSet rs ;
//T的Class对象
private Class entityClass ;
public BaseDAO() {
//getClass() 获取Class对象,当前我们执行的是new FruitDAOImpl() , 创建的是FruitDAOImpl的实例
//那么子类构造方法内部首先会调用父类(BaseDAO)的无参构造方法
//因此此处的getClass()会被执行,但是getClass获取的是FruitDAOImpl的Class
//所以getGenericSuperclass()获取到的是BaseDAO的Class
Type genericType = getClass().getGenericSuperclass();
//ParameterizedType 参数化类型
Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
//获取到的<T>中的T的真实的类型
Type actualType = actualTypeArguments[0];
try {
entityClass = Class.forName(actualType.getTypeName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new DAOException("BaseDAO 构造方法出错了,可能的原因是没有指定<>中的类型");
}
}
protected Connection getConn(){
return ConnUtil.getConn();
}
protected void close(ResultSet rs , PreparedStatement psmt , Connection conn){
}
//给预处理命令对象设置参数
private void setParams(PreparedStatement psmt , Object... params) throws SQLException {
if(params!=null && params.length>0){
for (int i = 0; i < params.length; i++) {
psmt.setObject(i+1,params[i]);
}
}
}
//执行更新,返回影响行数
protected int executeUpdate(String sql , Object... params) {
boolean insertFlag = false ;
insertFlag = sql.trim().toUpperCase().startsWith("INSERT");
conn = getConn();
try{
if(insertFlag){
psmt = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
}else {
psmt = conn.prepareStatement(sql);
}
setParams(psmt,params);
int count = psmt.executeUpdate() ;
if(insertFlag){
rs = psmt.getGeneratedKeys();
if(rs.next()){
return ((Long)rs.getLong(1)).intValue();
}
}
return 0 ;
}catch (SQLException e){
e.printStackTrace();
throw new DAOException("BaseDAO executeUpdate出错了");
}
}
//通过反射技术给obj对象的property属性赋propertyValue值
private void setValue(Object obj , String property , Object propertyValue) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
Class clazz = obj.getClass();
//获取property这个字符串对应的属性名 , 比如 "fid" 去找 obj对象中的 fid 属性
Field field = clazz.getDeclaredField(property);
if(field!=null){
//获取当前字段的类型名称
String typeName = field.getType().getName();
//判断如果是自定义类型,则需要调用这个自定义类的带一个参数的构造方法,创建出这个自定义的实例对象,然后将实例对象赋值给这个属性
if(isMyType(typeName)){
//假设typeName是"com.atguigu.qqzone.pojo.UserBasic"
Class typeNameClass = Class.forName(typeName);
Constructor constructor = typeNameClass.getDeclaredConstructor(java.lang.Integer.class);
propertyValue = constructor.newInstance(propertyValue);
}
field.setAccessible(true);
field.set(obj,propertyValue);
}
}
private static boolean isNotMyType(String typeName){
return "java.lang.Integer".equals(typeName)
|| "java.lang.String".equals(typeName)
|| "java.util.Date".equals(typeName)
|| "java.sql.Date".equals(typeName);
}
private static boolean isMyType(String typeName){
return !isNotMyType(typeName);
}
//执行复杂查询,返回例如统计结果
protected Object[] executeComplexQuery(String sql , Object... params){
conn = getConn() ;
try{
psmt = conn.prepareStatement(sql);
setParams(psmt,params);
rs = psmt.executeQuery();
//通过rs可以获取结果集的元数据
//元数据:描述结果集数据的数据 , 简单讲,就是这个结果集有哪些列,什么类型等等
ResultSetMetaData rsmd = rs.getMetaData();
//获取结果集的列数
int columnCount = rsmd.getColumnCount();
Object[] columnValueArr = new Object[columnCount];
//6.解析rs
if(rs.next()){
for(int i = 0 ; i<columnCount;i++){
Object columnValue = rs.getObject(i+1); //33 苹果 5
columnValueArr[i]=columnValue;
}
return columnValueArr ;
}
}catch(SQLException e){
e.printStackTrace();
throw new DAOException("BaseDAO executeComplexQuery出错了");
}
return null ;
}
//执行查询,返回单个实体对象
protected T load(String sql , Object... params){
conn = getConn() ;
try{
psmt = conn.prepareStatement(sql);
setParams(psmt,params);
rs = psmt.executeQuery();
//通过rs可以获取结果集的元数据
//元数据:描述结果集数据的数据 , 简单讲,就是这个结果集有哪些列,什么类型等等
ResultSetMetaData rsmd = rs.getMetaData();
//获取结果集的列数
int columnCount = rsmd.getColumnCount();
//6.解析rs
if(rs.next()){
T entity = (T)entityClass.newInstance();
for(int i = 0 ; i<columnCount;i++){
String columnName = rsmd.getColumnName(i+1); //fid fname price
Object columnValue = rs.getObject(i+1); //33 苹果 5
setValue(entity,columnName,columnValue);
}
return entity ;
}
}catch (Exception e){
e.printStackTrace();
throw new DAOException("BaseDAO load出错了");
}
return null ;
}
//执行查询,返回List
protected List<T> executeQuery(String sql , Object... params){
List<T> list = new ArrayList<>();
conn = getConn() ;
try{
psmt = conn.prepareStatement(sql);
setParams(psmt,params);
rs = psmt.executeQuery();
//通过rs可以获取结果集的元数据
//元数据:描述结果集数据的数据 , 简单讲,就是这个结果集有哪些列,什么类型等等
ResultSetMetaData rsmd = rs.getMetaData();
//获取结果集的列数
int columnCount = rsmd.getColumnCount();
//6.解析rs
while(rs.next()){
T entity = (T)entityClass.newInstance();
for(int i = 0 ; i<columnCount;i++){
String columnName = rsmd.getColumnLabel(i+1); //fid fname price
Object columnValue = rs.getObject(i+1); //33 苹果 5
setValue(entity,columnName,columnValue);
}
list.add(entity);
}
}catch (Exception e){
e.printStackTrace();
throw new DAOException("BaseDAO executeQuery出错了");
}
return list ;
}
}
Service
创建各类服务层,调用各类DAO实现类,用其中的方法完成各类业务操作。
以UserBasicService为例,完成登录和通过特定用户信息查询其好友列表
UserBasicService接口
package com.atguigu.qqzone.service;
import com.atguigu.qqzone.dao.UserBasicDAO;
import com.atguigu.qqzone.pojo.UserBasic;
import java.util.List;
public interface UserBasicService {
UserBasic login(String loginId , String pwd );
List<UserBasic> getFriendList(UserBasic userBasic);
}
UserBasicService实现类
package com.atguigu.qqzone.service.impl;
import com.atguigu.qqzone.dao.UserBasicDAO;
import com.atguigu.qqzone.pojo.UserBasic;
import com.atguigu.qqzone.service.UserBasicService;
import java.util.ArrayList;
import java.util.List;
public class UserBasicServiceImpl implements UserBasicService {
private UserBasicDAO userBasicDAO = null ;
@Override
public UserBasic login(String loginId, String pwd) {
UserBasic userBasic = userBasicDAO.getUserBasic(loginId,pwd);
return userBasic;
}
@Override
public List<UserBasic> getFriendList(UserBasic userBasic) {
List<UserBasic> userBasicList = userBasicDAO.getUserBasicList(userBasic);
List<UserBasic> friendList = new ArrayList<>(userBasicList.size());
for (int i = 0; i < userBasicList.size(); i++) {
UserBasic friend = userBasicList.get(i);
friend = userBasicDAO.getUserBasicById(friend.getId());
friendList.add(friend);
}
return friendList;
}
}
3.2 配置applicationContext.xml
通过依赖注入DI,在Tomcat启动时读取该配置文件,提前创建相应运行时service对象,根据配置文件中的bean,根据客户端发送的不同参数dispatcher中央控制器会使用不同的controller,调用不同的service来完成相应业务。
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE beans [
<!ELEMENT beans (bean*)>
<!ELEMENT bean (property*)>
<!ELEMENT property (#PCDATA)>
<!ATTLIST bean id ID #REQUIRED>
<!ATTLIST bean class CDATA #IMPLIED>
<!ATTLIST property name CDATA #IMPLIED>
<!ATTLIST property ref IDREF #IMPLIED>
]>
<beans>
<bean id="userBasicDAO" class="com.atguigu.qqzone.dao.impl.UserBasicDAOImpl"/>
<bean id="topicDAO" class="com.atguigu.qqzone.dao.impl.TopicDAOImpl"/>
<bean id="userBasicService" class="com.atguigu.qqzone.service.impl.UserBasicServiceImpl">
<property name="userBasicDAO" ref="userBasicDAO"/>
</bean>
<bean id="topicService" class="com.atguigu.qqzone.service.impl.TopicServiceImpl">
<property name="topicDAO" ref="topicDAO"/>
</bean>
<bean id="user" class="com.atguigu.qqzone.controller.UserController">
<property name="userBasicService" ref="userBasicService"/>
<property name="topicService" ref="topicService"/>
</bean>
<bean id="page" class="com.atguigu.myssm.myspringmvc.PageController"/>
</beans>
XML 验证
拥有正确语法的 XML 被称为"形式良好"的 XML。
通过 DTD 验证的XML是"合法"的 XML。
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE beans [
<!ELEMENT beans (bean*)>
<!ELEMENT bean (property*)>
<!ELEMENT property (#PCDATA)>
<!ATTLIST bean id ID #REQUIRED>
<!ATTLIST bean class CDATA #IMPLIED>
<!ATTLIST property name CDATA #IMPLIED>
<!ATTLIST property ref IDREF #IMPLIED>
]>
3.3 修改login.html中的action
对登录页面,提交的是user.do,调用userBasicService、和topicService,经过thymeleaf渲染实现登录后页面。
th:action="@{/user.do}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/login.css">
</head>
<body>
<div id="div0">
<div id="div_container">
<p class="center">用户登录1</p>
<form th:action="@{/user.do}💥💥" method="get">
<input type="hidden" name="operate" value="login"/>
<table>
<tr>
<th>用户名:</th>
<td><input type="text" name="loginId" value="u001"/></td>
</tr>
<tr>
<th>密码:</th>
<td><input type="password" name="pwd" value="ok"/></td>
</tr>
<tr>
<th colspan="2">
<input type="submit" value="登录"/>
<input type="button" value="还没有账号?"/>
</th>
</tr>
</table>
</form>
</div>
</div>
</body>
</html>