目录
1.熟悉QQZone业务需求
2.数据库设计
3.数据库的范式
4. 登录功能的主体实现
5. 进入好友页面和返回自己页面功能
6. 日志功能完成
7.目前我们进行javaweb项目开发的“套路”
1) 用户登录
2) 登录成功,显示主界面。左侧显示好友列表;上端显示欢迎词。如果不是自己的空间,显示超链接:返回自己的空间;下端显示日志列表
3) 查看日志详情:
- 日志本身的信息(作者头像、昵称、日志标题、日志内容、日志的日期)
- 回复列表(回复者的头像、昵称、回复内容、回复日期)
- 主人回复信息
4) 删除日志
5) 删除特定回复
6) 删除特定主人回复
7) 添加日志、添加回复、添加主人回复
8) 点击左侧好友链接,进入好友的空间
1) 抽取实体 : 用户登录信息、用户详情信息 、 日志 、 回贴 、 主人回复
2) 分析其中的属性:
- 用户登录信息:账号、密码、头像、昵称
- 用户详情信息:真实姓名、星座、血型、邮箱、手机号.....
- 日志:标题、内容、日期、作者
- 回复:内容、日期、作者、日志
- 主人回复:内容、日期、作者、回复
3) 分析实体之间的关系
- 用户登录信息 : 用户详情信息 1:1 PK
- 用户 : 日志 1:N
- 日志 : 回复 1:N
- 回复 : 主人回复 1:1 UK
- 用户 : 好友 M : N
1) 第一范式:列不可再分
2) 第二范式:一张表只表达一层含义(只描述一件事情)
3) 第三范式:表中的每一列和主键都是直接依赖关系,而不是间接依赖
数据库设计的范式和数据库的查询性能很多时候是相悖的,我们需要根据实际的业务情况做一个选择:
- 查询频次不高的情况下,我们更倾向于提高数据库的设计范式,从而提高存储效率
- 查询频次较高的情形,我们更倾向于牺牲数据库的规范度,降低数据库设计的范式,允许特定的冗余,从而提高查询的性能
建立数据库和表
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;
数据库一共六个表,第一个是 t_user_basic(用户基本信息表)主键id(多次作为别的表的外键),这里说一下一般把自增的无意义的主键作为其他从表的外键,因为害怕防止数据库合并出现的数据重复问题
第二个是 t_user_detail(用户详细信息表)t_user_basic 的主键 id 为它 id 的外键,目前为空
第三个是 t_topic(日志表)t_user_basic的主键 id 为它 author 的外键
第四个是 t_reply(回复表)t_user_basic的主键 id 为它 author 的外键, t_topic的主键 id 为它 topic 的外键
第五个是 t_host_reply(主人回复表)t_user_basic的主键 id 为它 author 的外键, t_reply的主键 id 为它 reply 的外键 ,目前为空
第六个是 t_friend(朋友表)t_user_basic的主键 id 既为它 fid 的外键,还是 uid 的外键
创建新模块,并把前端部分复制过来,同时把上一个项目自己写的myssm包下的可以被重复使用的部分拿过来建立我们的后端框架,并建立qqzone包将来存储和这个项目相关的dao,pojo,service等文件,同时也别忘了把我们的druid配置文件和beanFactory的配置文件复制过来,并且改写里面的内容,如数据库名称
同时我们的web.xml文件也需要写入我们之前项目给Thymeleaf和BeanFactory的
view-prefix
/
view-suffix
.html
contextConfigLocation
applicationContext.xml
然后开始给我们的qqzone项目,对应E-R图部分写POJO
POJO(Plain Ordinary Java Object)简单的Java对象,实际就是普通JavaBeans,使用POJO名称是为了避免和EJB混淆起来, 而且简称比较直接. 其中有一些属性及其getter setter方法的类,没有业务逻辑,有时可以作为VO(value -object)或DTO(Data Transform Object)来使用.当然,如果你有一个简单的运算属性也是可以的,但不允许有业务方法,也不能携带有connection之类的方法。
首先是UserBasic,把E-R图的数量对应关系的方法也写入其中(这里把get set方法删去为了博客阅读方便),记得后面的也一样,千万不能忘记写空参构造方法(为了能被反射使用)
同时这里涉及到和其他表相关的属性,如外键直接使用对象类,SQL查询的时候调用对应的对象的get方法即可
public class UserBasic {
private Integer id;
private String loginId;
private String nickname;
private String pwd;
private String headImg;
private UserDetail userDetail; // 1 : 1
private List topicList; // 1 : N
private List friendList; // M : N
public UserBasic() {}
}
这里Date因为是信息,使用sql下的Date,而日志和回复等地方的Date需要精确时间,所以用util下的
java.util.Date 年月日 时分秒 毫秒
|------> java.sql.Date 年月日
|------> java.sql.Time 时分秒
public class UserDetail {
private Integer id;
private String realName;
private String tel;
private String email;
private Date birth;
private String star;
public UserDetail() {}
}
public class Topic {
private Integer id;
private String title;
private String content;
private Date topicDate;
private UserBasic author; // M : 1
private List replyList; // 1 : N
public Topic() {}
}
public class Reply {
private Integer id;
private String content;
private Date replyDate;
private UserBasic author; // M : 1
private Topic topic; // M : 1
private HostReply hostReply; // 1 : 1
public Reply() {
}
}
public class HostReply {
private Integer id;
private String content;
private Date hostReplyDate;
public HostReply() {}
}
然后给qqzone项目写对应的DAO层的接口和实现类,这里我们优先完成登录模块的任务,所以先写或者实现和登录相关的功能,其他的方法和功能日后再写
登录成功后左侧显示一列好友列表,所以写登录功能和查看好友列表功能
public interface UserBasicDAO {
// 根据账号和密码获取特定用户信息
public UserBasic getUserBasic(String loginId, String pwd);
// 获取指定用户的所有好友列表
public List getUserBasicList(UserBasic userBasic);
// 根据ID查询指定用户的信息
public UserBasic getUserBasicById(Integer id);
}
登录成功后右侧显示日志列表,所以写和日志相关的主体功能
public interface TopicDAO {
// 根据用户获取所有日志列表
public List getTopicList(UserBasic userBasic);
// 添加日志
public void addTopic(Topic topic);
// 删除日志
public void delTopic(Topic topic);
// 获取特定日志信息
public Topic getTopic(Integer id);
}
根据日志获取指定的回复应该写在Reply的DAO下,查询和哪个表相关的信息,写在哪个DAO下
public interface ReplyDAO {
//获取指定日志的回复列表
public List getReplyList(Topic topic);
//添加回复
public void addReply(Reply reply);
//删除回复
public void delReply(Integer id);
}
这里的方法和登录无关我们就先不写
public interface HostReplyDAO {
//
}
DAO层的实现类,首先UserBasicDAOImpl,我们继承我们的JDBC封装的BaseDao和刚写的接口,实现里面的功能,这里try-catch的异常我们为了能事后通过Filter完成事务操作,可以选择再throw出去一个异常,还可以让我们知道异常是什么位置发生
其次我们查询一个用户的好友列表,可以使用多表外连接查询他们的细节信息
SELECT *
FROM t_user_basic t1
LEFT JOIN t_friend t2 ON t1.id = t2.uid
LEFT JOIN t_user_basic t3 ON t2.fid = t3.id;
这里我们为了语句简单,使用了简单的单表查询,查询 t_friend表的 fid,但是我们返回的List泛型里面是UserBasic类,方法填入的类也是如此,我们是通过反射获取对应类的属性,然后JbdcUtils获取的连接池的连接,同时BaseDao获取结果集,然后我们通过结果集的MetaData获取列数量,列名,然后通过循环给类中和列名相同的字段赋值,这也是为什么我们要把POJO的属性名写的和数据库列名一样
但是我们这里查的是 t_friend 表,UserBasic没有fid这个字段,所以我们查询语句需要写别名携程它有的id,那么我们通过反射只能给类的实例赋值这一个字段,其他的都为默认值,List中的一个个对象只有这个字段有属性
这里显然有些问题,其实可能老师这里为了遵循阿里的规范,尽量不用多表查询,而是在业务层获取这个只有id属性有值的List后再遍历通过Dao的根据id获取详细信息的方法来实现,所以DAO这里还写了个根据id返回List的方法,方便我们业务层通过这个方法,获取朋友列表
public class UserBasicDAOImpl extends BaseDao implements UserBasicDAO {
@Override
public UserBasic getUserBasic(String loginId, String pwd){
String sql = "SELECT * FROM t_user_basic WHERE login_id = ? AND pwd = ?;";
List userBasics = null;
try {
userBasics = executeQuery(UserBasic.class, sql, loginId, pwd);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("UserBasic.getUserBasic出现问题!");
}
if (userBasics!= null && userBasics.size() > 0) {
return userBasics.get(0);
}
return null;
}
@Override
public List getUserBasicList(UserBasic userBasic) {
String sql = "SELECT fid AS id FROM t_friend WHERE uid = ?";
try {
return executeQuery(UserBasic.class, sql, userBasic.getId());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("UserBasic.getUserBasicList出现问题!");
}
}
@Override
public UserBasic getUserBasicById(Integer id) {
String sql = "SELECT * FROM t_user_basic WHERE id = ?;";
List userBasics = null;
try {
userBasics = executeQuery(UserBasic.class, sql, id);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("UserBasic.getUserBasicById出现问题!");
}
if (userBasics!= null && userBasics.size() > 0) {
return userBasics.get(0);
}
return null;
}
}
这里和登录无关的部分我们就先不实现
public class TopicDAOImpl extends BaseDao implements TopicDAO {
@Override
public List getTopicList(UserBasic userBasic) {
String sql = "SELECT * FROM t_topic WHERE author = ?;";
try {
return executeQuery(Topic.class, sql, userBasic.getId());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("TopicDAOImpl.getTopicList出现异常!");
}
}
@Override
public void addTopic(Topic topic) {}
@Override
public void delTopic(Topic topic) {}
@Override
public Topic getTopic(Integer id) {return null;}
}
此时我们需要写一下application.xml配置文件,把我们预先要加载的Bean对象写进入,这里我们只写了目前的两个Dao实现类,就把他们写进去,让Listenner可以容器创建之初就把他们生成到BeanMap中
接着我们来写业务层Service方法
public interface UserBasicService {
//登录方法
public UserBasic login(String loginId, String pwd);
//获取好友列表方法
public List getFriendList(UserBasic userBasic);
}
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 getFriendList(UserBasic userBasic) {
List userBasicList = userBasicDAO.getUserBasicList(userBasic);
List friendList = new ArrayList(userBasicList.size());
for (UserBasic friend : userBasicList) {
friend = userBasicDAO.getUserBasicById(friend.getId());
friendList.add(friend);
}
return friendList;
}
}
public interface TopicService {
public List getTopicList(UserBasic userBasic);
}
public class TopicServiceImpl implements TopicService {
private TopicDAO topicDAO = null;
@Override
public List getTopicList(UserBasic userBasic) {
return topicDAO.getTopicList(userBasic);
}
}
然后再把Service的Bean写入applicationContext.xml文件里面去,因为我们的业务层调用了DAO层,内部有私有的DAO层的实例,所以需要给Bean写内部属性,name是实例的名字,ref是在Bean文件的id
接着我们就该写Controller组件了,我们目前实现的是登录功能,就先写UserController,首先我们需要登录,就写登录的方法login,登录成功从login.html转入index.html,而登录失败继续返回登录页面。登录需要获取好友列表和日志列表,调用我们刚刚写的Service层的方法去完成它,这里肯定需要写私有的实例对象,这个得写入applicationContext.xml下,作为属性,方便我们的监听器创建实例存入
public class UserController {
private UserBasicService userBasicService;
private TopicService topicService;
public String login(String loginId, String pwd, HttpSession session) {
//1.登录验证
UserBasic userBasic = userBasicService.login(loginId, pwd);
//1-1 获取相关的好友信息
if(userBasic!= null){
List friendList = userBasicService.getFriendList(userBasic);
//1-2 获取相关的日志列表信息
List topicList = topicService.getTopicList(userBasic);
userBasic.setFriendList(friendList);
userBasic.setTopicList(topicList);
session.setAttribute("userBasic", userBasic);
return "index";
}else{
return "login";
}
}
}
写入applicationContext.xml,这里!DOCTYPE是对内部格式加限制,不要求掌握,日后想学可以去学。这里写的很容易看懂
]>
这里我们和之前学习的一样,这个login方法要返回一个字符串,我们的DispatcherServlet通过反射会在init()的时候获取BeanFactory实例,这个实例在ContextLoaderLstener在tomcat容器创建之初就通过监听器先创建application作用域对象,然后读取我们写在web.xml下的name为contextConfigLocation的
DispatcherServlet在service的方法下,也就是默认服务状态,会监听到request,然后读取其中的servlet的path,然后解析path中的字符串值,通过BeanMap先获取同名的实例,然后就取到了相应的Controller对象实例,故我们也需要在applicationContext.xml下写Controller,然后通过反射获取Controller的方法,和参数列表,然后把请求中同名的参数赋值到实例中(这里其实还经过了类型判断和类型转化,需要我们给虚拟机设置参数,让它能把反射获取的参数列表的类型也带过来,以字符串形式),当然,如果是req或者是resp或者session,我们就直接把Servlet获取的req或resp,或通过req获取的session赋值给Controller实例对象。最终设定能访问私有方法,通过反射的invoke调用方法,然后再通过视图处理渲染页面或者是重定向操作(通过获取到的方法返回的字符串有无redirect:前缀)
现在我们发送user.do的请求给DispatcherServlet会让它跳转到UserController,我们现在让它跳转登录页面,于是我们需要把登录页面的login.html写入Thymeleaf去让它渲染。给输入账号密码的form表单的action加入th:{@}然后写入user.do(这里不加Thymeleaf语法是为了我们方便我们先去调试,tomcat直接访问静态的登录页面,让我们从后台观察运转情况,后续改为Thymeleaf语法,方便直接进行渲染),同时这里是Get的方法去request。同时别忘了把html的命名空间加入,可以按 alt + enter,写入http://www.thymeleaf.org
的隐藏域也是一定不能忘了加,因为我们需要通过operate获取到到底是哪个方法
Title
用户登录1
我们现在试着在tomcat上试着运行一下,发现在反射前面获取参数和参数列表,以及session都没有问题,但在最后通过反射给字段赋值的时候,出现了错误。
经过Debug发现是因为Mysql8中数据库的DateTime类型映射到Java中对应的是LocalDateTime,不能改为java.sql.util下的Date,于是这里回去把所有的Pojo的为util下Date的日期类型改为了LocalDateTime
再经过修改锁定报错位置在tpicDAO层的反射,我们发现我们从数据库获取的author值是Integer类型的,而我们实际写JavaBean对象的时候写的是UserBasic类,这时我们的BaseDao的方法只能根据数据库到底是什么类型去给实际的Java对象去赋值
这里老师采用的方法是改写BaseDAO方法,给BaoDAO加入一个方法,判断是不是我们数据库自定义的类型,如果不是的话,就还按之前的方法直接根据数据库获取的类型给Java对象的字段赋值
如果不是通过反射获取到底Pojo的内部这个author字段类型是什么(这里是UserBasic),直接给找个UserBasic加个只有形参类型Integer id的构造方法,然后通过反射调用它的这个构造方法创建一个UserBasic实例,然后再把这个实例通过反射set给我们的Topic实例对象
加入的判断类型的方法
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)
|| "java.time.LocalDateTime".equals(typeName);
}
原本的赋值部分
while (resultSet.next()) {
//调用T的无参构造函数生成对象
T t = clazz.newInstance();
//自动取值 即便是换一个数据表,也是相同的操作
for (int i = 1; i <= columnCount; i++) {
//对象的属性值
Object value = resultSet.getObject(i);
//getColumnLabel :会获取别名,无别名获取列名 getColumName 只会获取列的名称
String columnName = metaData.getColumnLabel(i);
//反射给对象的属性值赋值
Field field = clazz.getDeclaredField(columnName);
field.setAccessible(true);
field.set(t, value);
}
//将对象存入list集合中即可
list.add(t);
}
修改后赋值这部分
while (resultSet.next()) {
//调用T的无参构造函数生成对象
T t = clazz.newInstance();
//自动取值 即便是换一个数据表,也是相同的操作
for (int i = 1; i <= columnCount; i++) {
//对象的属性值
Object value = resultSet.getObject(i);
//获取指定下角标列的名称
//getColumnLabel :会获取别名,无别名获取列名 getColumName 只会获取列的名称
String columnName = metaData.getColumnLabel(i);
//反射给对象的属性值赋值
Field field = clazz.getDeclaredField(columnName);
//获取它的类型名称
String typeName = field.getType().getName();
if(isNotMyType(typeName)){
Class typeNameClass = Class.forName(typeName);
Constructor constructor = typeNameClass.getDeclaredConstructor(Integer.class);
value = constructor.newInstance(value);
}
field.setAccessible(true);
field.set(t, value);
}
//将对象存入list集合中即可
list.add(t);
}
现在我们就解决了这个问题,彻底把login页面的操作完成,接下来根据DispatcherServlet的service方法,我们会返回UserController的login的方法的返回值,也就是index,然后通过thymeleaf渲染,现在我们就开始写index页面
index.html这里发现左右表的src返回了两个html文件,我们就先去left.html,这里我们使用Thymeleaf语法修改它,把css路径用 th:href="@{}"修饰,然后需要展示当前用户好友的名字,所以我们也使用Thymeleaf语法,进行 th:if 判断是否好友列表的这个属性List为空
如果不为空我们就使用th:each循环遍历friendList,然后通过 th:text 渲染这个列表中的元素的nickName属性
Title
我的好友2
- 一个好友也没有
此时运行还是无法显示,这里就是咋们上一个项目中出现的老问题,我们跳转到html直接申请的是静态页面,而不是通过Servlet让它传给Thymeleaf渲染
我们想让它跳转经过Servlet,不如我们单独给这个跳转页面写一个PageController,因为是通用的(后面的静态跳转改动态都可以通过它来实现),我们就不放在controller包下,放在myspringmvc下,看似代码很奇怪,方法名叫page,参数也是page,返回了这个参数的字符串
其实依赖于我们的DispatcherServlet的缘故,它会通过反射先找Controller的方法名,用的是operate字段,而它也会通过反射获取这个方法的形参列表和返回值,在request中找寻和这个字段名称相同的参数值,把request的值赋值给方法并调用,然后获取它的返回值,这个返回值是用来经过Thymeleaf渲染的
public class PageController {
public String page(String page){
return page;
}
}
既然它是Controller我们肯定需要把它写到applicationContext.xml文件里面,让容器创建之初就把这个对象建立
所以我们后面的index.html的左右表的src就按照上面写的逻辑去写首先,page.do会让DispatcherServlet解析到page,然后去BeanMap里面获取key值为page的对象,然后operate=page使得我们DispacherServlet解析到page方法,然后获取它形参列表的参数名为page,于是把request这段page=的值: frames/left获取赋值给这个形参,然后这个方法就返回这个值,返回的值,就被我们用来使用Thymeleaf渲染,于是终于我们的好友列表可以让它渲染成功了
现在我们就可以不直接访问静态的login.html了,之前说过的表单没写Thymeleaf的地方加上
最终,我们修改访问网页的方式就是通过这个Controller动态访问,只需要写入合适的参数如下即可,具体逻辑上面解释过了
至此左表加载完成,同时登录也差不多完成,也彻底回答了之前fruit项目出现的Thymeleaf的问题
现在开始做登录后的主页面的日志列表的展示,我们现在通过给BaoDAO的封装,哪怕是author返回类型为UserBasic, 也能通过反射给相应的类型获取到赋值,那我们就可以直接做main.html的Thymeleaf的加载了
我们打开对应的main.html,首先在最上面的html的标签里面,加入xmlns:th="http://www.thymeleaf.org",让Thymeleaf的语法能自检,然后把css的href加入th:href="@{}",然后日志的信息我们的UserController会通过BeanMap调用UserBasicServiceimpl的方法,而这个实例会调用Dao层的方法,查询到topicList并作为一个对象存入UserBsic对象的属性,然后存入session作用域
和前面一样,主页面的日志这里的展示是在main.html中,需要使用Thymeleaf的逻辑判断th:if="${#lists.isEmpty(session.userBasic.topicList)}",不为空再unless,然后遍历TopicList,把相应信息展示,这里的删除日志的功能,暂时不做
Title
发表新日志
ID
标题
日期
操作
暂无日志列表
2
我乔峰要走,你们谁可阻拦
2021-09-01 12:30:55
删除
别忘了index主页面的html给日志的div的src使用Thymeleaf改写
现在登录页面的好友列表和日志列表都能正常显示了
我们在数据库查询到Jim的UserBasic表中的主键id为1,然后查询日志表,发现确实把它的日志展示了
然后我们在数据库给它加一条数据看看数据多条是不是也能正常显示
刷新页面可以看到数据成功添加并展示到了后台
这里其实我小小的思考了一个很重要的问题,为什么我们之前的水果系统加数据没有实时的显示,这里就实时显示了:之前我们做的是前台加水果数据,通过Service调用DAO添加了,但是,我们那会还没写redirect的返回方法也没有DispacherServlet,那会返回了还是之前渲染过的动态页面,但是session作用域的水果列表还是没有得到更新,所以我们添加了重查的redirect:标识,让渲染的时候,遇到它使用resp.sendRedirect()让客户端重新发送请求,通过Servlet查询,但是这里不需要,因为我们是直接在网页端发送,也写好并一定会经过DispatcherServlet,然后重查刷新作用域
小复盘,这一节我们主要做了
1. 根据数据库的字段和分析的E-R图建立POJO的类,同时他们关联的字段我们选择存相应的主表对象而不是Integer 的id,因此只能修改BaseDAO加入通过反射把UserBasic对象赋值在Java端的方法,但是我们只搜到id,故需要添加一个只有id的构造器,这里通过反射new的对象只有id有值
2. left.html页面没有样式,同时数据也不展示,原因是:我们是直接去请求的静态页面资源,那么并没有执行super.processTemplate(),也就是thymeleaf没有起作用
(之前的表单也是这个原因)
解决方法:
- 新增PageController,添加page方法:
public String page(String page){
return page ; // frames/left
}
目的是执行super.processTemplate()方法,让thymeleaf生效3. 完成了登录功能,和登录后跳转到index页面,同时展示主账户的日志列表和好友列表
复制一个新的模块,记得添加工件和修改tomcat的配置工件
我们index页面顶部的欢迎栏,还没有配置,这里左边的好友列表和日志列表已经使用thymeleaf语法把src路径写好了
这里先加入thymeleaf修饰这个top.html的src,让它能经过thymeleaf渲染
尤其是别忘了把top.html中的css加入thymeleaf的语法,加入路径
Title
欢迎来到QQZone!
欢迎进入Jim的空间!
返回自己的空间!
我们功能需要显示欢迎来到对应的用户的空间的文字,以及返回自己空间的链接。(后台加判断,是否是自己的空间,不是自己的空间需要能够出现返回的链接) 这个功能怎么实现呢?老师这里提供思路,我们给session作用域不只保存一份名为userBasic的对象,还保存一份名为friend的对象。这样我们到一个页面的时候,只需要判断当前进入这个页面这两个对象是不是相同的,如果不同说明去到了别人的页面。userBasic这个key保存的是登录者的信息 ,friend这个key保存的是当前进入的是谁的空间
public class UserController {
private UserBasicService userBasicService = null;
private TopicService topicService = null;
public String login(String loginId, String pwd, HttpSession session) {
//1.登录验证
UserBasic userBasic = userBasicService.login(loginId, pwd);
//1-1 获取相关的好友信息
if(userBasic!= null){
List friendList = userBasicService.getFriendList(userBasic);
//1-2 获取相关的日志列表信息(但是,日志只有id,没有其他信息)
List topicList = topicService.getTopicList(userBasic);
userBasic.setFriendList(friendList);
userBasic.setTopicList(topicList);
//userBasic这个key保存的是登录者的信息
session.setAttribute("userBasic", userBasic);
//friend这个key保存的是当前进入的是谁的空间
session.setAttribute("friend", userBasic);
return "index";
}else{
return "login";
}
}
}
修改为如下
Title
欢迎来到QQZone!
欢迎进入Jim的空间!
返回自己的空间!
如果出问题,这里调试的思路可以先从输出角度,看看session作用域下能不能找到这俩对象和他们的属性
userBasic:
friend:
然后我们接着完善左表,我们之前只做到显示好友列表,但是没有实现点击名字的链接跳转到好友的页面去,所以我们需要给那个文本加超链接,但是那个列表的标签写了th:text="${...}",我们不能直接往里面写超链接,要不然会被覆盖,所以我们把这个th:text="${...}"写到超链接里面,但是我们这里应该怎么填?
这里陷入一个思考,我们这个friendList中的好友,他们不是也有friendList,我们岂不是递归的查询到了朋友的日志列表和朋友列表?那岂不是什么信息都有了,所有地方都能直接通过get方法获取了。
其实这里思考了很久,仔细一想,我们登录是用的我们userBasicDAO的getUserBasic(String loginId, String pwd)方法然后从表里查到的信息赋值给POJO的UserBasic对象,这里属性都是能从SQL查到的Object然后转化的。
此时朋友列表等很多我们定义的自定义的根据E-R图定义的属性字段,并不会从DAO查到,而是通过Service层。所以获取朋友列表是通过Service层,调用userBasicDAO的getUserBasicList()方法,但是DAO层的这个方法其实我们是通过t_friend表,查到的只有id的信息,所以BaoDAO反射赋值的这个List
因此这些朋友的信息是通过Service层的新方法getFriendList(UserBasic userBasic)获取的,把主用户传入,遍历它的friend的反射然后再调用一次DAO层,然后此时调用我们的UserBasicDAO的getUserBasicById(Integer id)方法然后查询到的他们数据库有的信息,所以终于话归正题,这些朋友只有数据库有的信息,而他们的FriendList也是空的,至此这个很复杂又很绕的部分我们就搞定了。
找到去好友空间的途径,我们肯定先需要获取好友的好友的信息,我们这里边写边思考,先写UserController,我们之前已经完成了login方法,这次我们写一个friend方法(去别人空间就不需要账号密码了,而且肯定也不能修改别人空间,或者在别人空间发Topic)
老师认为这里会传入一个id,然后根据朋友的id去获取这个用户(使用id就是因为id为主键,查询快),同时我们肯定需要从session作用域获取信息,参数就先写(Integer id, HttpSession session),同时我们业务层根据id获取UserBasic还没写这个方法,现在补上,先给接口写。
public interface UserBasicService {
//登录方法
public UserBasic login(String loginId, String pwd);
//获取好友列表方法
public List getFriendList(UserBasic userBasic);
//根据id获取指定的用户信息
public UserBasic getUserBasicById(Integer id);
}
然后在实现类去实现,肯定要使用DAO层的方法,这个方法我们之前为了让只有id的朋友空表获取属性的时候写了,直接调用即可。
@Override
public UserBasic getUserBasicById(Integer id) {
return userBasicDAO.getUserBasicById(id);
}
现在可以在friend函数里面获取好友具体信息了,下一步是获取他所有的日志,我们之前已经为了完成登录在TopicService上了,我们直接调用获取日志列表,然后通过set方法给朋友添加这个日志的属性,此时我们传入session给什么字段?我们之前已经设置是否显示返回自己的空间,是靠判断一个friend字段和userBasic字段是否相等,这个friend字段就是在这里等着我们的,所以是通过session.setAtrribute覆盖friend的value。
public String friend(Integer id, HttpSession session){
//1 根据id获取指定朋友的信息
UserBasic currFriend = userBasicService.getUserBasicById(id);
//2 查询朋友的日志列表
List topicList = topicService.getTopicList(currFriend);
//3 然后通过set方法给朋友添加这个日志的属性
currFriend.setTopicList(topicList);
//4 此时我们传入session给friend字段
session.setAttribute("friend", currFriend);
return "index";
}
然后似乎就可以返回index渲染了?错,我们前端页面的日志还有好友列表的Thymeleaf渲染是通过session.userBasic,显然这里我们要回去修改一下,首先是日志列表,然后显然我们不应该能看到好友的好友列表,显然左表还应该是session.userBasic。
Title
发表新日志
ID
标题
日期
操作
暂无日志列表
2
我乔峰要走,你们谁可阻拦
2021-09-01 12:30:55
删除
别忘了我们这一系列操作都是为了点击左侧好友列表名字的超链接然后进行跳转的,我们的超链接还没填入信息,这个时候显然我们就可以让它通过DispatcherController的跳转方法跳转了。
这里小插曲使用Thymeleaf的时候,我们发现竖线的符号得写在@{}内部,而不是之前写在引号内,说明路径声明需要优先于Thymeleaf符号表达式,同时这里也发现进入别人空间这里忘记改成session.friend下了
欢迎进入Jim的空间!
上面是之前的一个使用的地方,下面是现在修改后的情况
张三
此时运行服务器,惊人的发现,虽然现在完成了这个进入朋友页面的功能,但是竟然显示到了左边的好友列表原本的页面了。
这是因为我们超链接的target属性默认是_self即把url定位到当前页面,而当前left.index是父节点为index.html的一个页面, 所以我们这里自定义改为target="_top",返回祖先节点
张三
此时就能进入好友页面了,然后头表的返回自己的空间部分还没做,此时就可以把这个返回自己空间的部分加上超链接,然后返回了,同时也记得给它加入target=“_top”
Title
欢迎来到QQZone!
欢迎进入Jim的空间!
返回自己的空间!
此时彻底完成了进入好友空间和返回自己空间的功能
小复盘,这一节我们主要做了
1. top.html页面显示登录者昵称、判断是否是自己的空间
1)显示登录者昵称: ${session.userBasic.nickName}
2)判断是否是自己的空间 : ${session.userBasic.id!=session.friend.id}
如果不是期望的效果,首先考虑将两者的id都显示出来2. 点击左侧的好友链接,进入好友空间
1) 根据id获取指定userBasic信息,查询这个userBasic的topicList,然后覆盖friend对应的value
2) main页面应该展示friend中的topicList,而不是userBasic中的topicList
3) 跳转后,在左侧(left)中显示整个index页面
- 问题:在left页面显示整个index布局
- 解决:给超链接添加target属性: target="_top" 保证在顶层窗口显示整个index页面4) top.html页面需要修改: "欢迎进入${session.friend}"
top.html页面的返回自己空间的超链接需要修改:
思路
1) 已知topic的id,需要根据topic的id获取特定topic
2) 获取这个topic关联的所有的回复
3) 如果某个回复有主人回复,需要查询出来
- 在TopicController中获取指定的topic
- 具体这个topic中关联多少个Reply,由ReplyService内部实现
4) 获取到的topic中的author只有id,那么需要在topicService的getTopic方法中封装,在查询topic本身信息时,同时调用userBasicService中的获取userBasic方法,给author属性赋值
5) 同理,在reply类中也有author,而且这个author也是只有id,那么我们也需要根据id查询得到author,最后设置关联
首先把之前静态的点击日志名称的超链接修改能经过Thymeleaf进入页面(href修改)
我乔峰要走,你们谁可阻拦
上面是main.html之前的样子,下面是修改之后的,可以看到Servlet请求改为了topic.do,还使用了要给名为topicDetail的方法
我乔峰要走,你们谁可阻拦
显然我们该去写TopicController了,显然我们需要调用业务层的方法,固然肯定需要写入私有业务层属性topicServic,然后我们这个topicDetail的方法,返回值肯定需要是String(dispathcerServlet渲染),然后我们根据之前的思路,需要传入一个id,然后获取对应的Topic,显然这个方法应该是业务层实现,所以给业务层接口写这个方法
public interface TopicService {
public List getTopicList(UserBasic userBasic);
public Topic getTopicById(Integer id);
}
然后给实现类写实现方法,当然肯定是调用DAO的方法,这里我们之前已经写过这个DAO方法了,可以直接return
public class TopicServiceImpl implements TopicService {
private TopicDAO topicDAO = null;
@Override
public List getTopicList(UserBasic userBasic) {
return topicDAO.getTopicList(userBasic);
}
@Override
public Topic getTopicById(Integer id) {
return topicDAO.getTopic(id);
}
}
但是我们DAO的实现类的这个方法是空的,return null接下来需要重写
@Override
public Topic getTopic(Integer id) {
String sql = "SELECT * FROM t_topic WHERE id = ?;";
List topics = null;
try {
topics = executeQuery(Topic.class, sql, id);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("TopicDAOImpl.getTopic出现异常!");
}
if (topics != null && topics.size() > 0) {
return topics.get(0);
}
return null;
}
TopicController这个时候肯定需要将查询到的Topic放入session域或者request作用域,因为我们一会要跳转到日志的详情页面,所以参数需要增加HttpSession,这里写 return “detail” 是不行的,因为我们根据Thymeleaf加载,在根目录下没有这个html文件,需要写上它的根目录frames
public class TopicController {
private TopicService topicService;
public String topicDetail(Integer id, HttpSession session){
Topic topic = topicService.getTopicById(id);
session.setAttribute("topic", topic);
return "frames/detail";
}
}
养成好习惯,写完DAO或者Service或者Controller层需要写入application.xml配置文件
]>
获取关联的所有回复这个功能我们还没实现,这个功能显然是个复杂的功能应该是Service层的,所以需要给ReplyService层写接口然后写这个方法
public interface ReplyService {
//根据topic的id获取全部的回复
public List getReplyList(Integer topicId);
}
然后实现它的时候意识到还没有写ReplyDAO接口的实现类,接口我们之前写好了,方法也存在
public interface ReplyDAO {
//获取指定日志的回复列表
public List getReplyList(Topic topic);
//添加回复
public void addReply(Reply reply);
//删除回复
public void delReply(Integer id);
}
实现类,这里其实提前预判到需要给Topic写带id的构造方法,因为Reply的POJO属性写的是Topic类型,还需要借助我们的BaoDAO之前封装的反射模块去完成这个类型的封装
public class ReplyDAOImpl extends BaseDao implements ReplyDAO {
@Override
public List getReplyList(Topic topic) {
String sql = "SELECT * FROM t_reply WHERE topic = ?;";
List replies = null;
try {
replies = executeQuery(Reply.class, sql, topic.getId());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("ReplyDAOImpl.getReplyList出现异常!");
}
return replies;
}
@Override
public void addReply(Reply reply) {
}
@Override
public void delReply(Integer id) {
}
}
现在业务层的实现类就可以直接写好了
public class ReplyServiceImpl implements ReplyService {
private ReplyDAO replyDAO;
@Override
public List getReplyList(Integer topicId) {
return replyDAO.getReplyList(new Topic(topicId));
}
}
现在获取对应Topic的全部reply功能已经没什么问题了,续写我们的TopicController
public class TopicController {
private TopicService topicService;
private ReplyService replyService;
public String topicDetail(Integer id, HttpSession session){
Topic topic = topicService.getTopicById(id);
List replyList = replyService.getReplyList(id);
topic.setReplyList(replyList);
session.setAttribute("topic", topic);
return "frames/detail";
}
}
现在获取全部Reply没问题,但是我们还需要根据每个Reply找到对应的主人回复HostReply,这个和获取全部Reply几乎是一个流程,我们先给它写Service接口,和实现类
public interface HostReplyService {
//获取一个Reply对应的全部HostReply
public HostReply getHostReplyByReplyId(Integer replyId);
}
然后实现它的时候意识到还没有写HostReplyDAO接口的实现类,接口我们之前写好了,方法之前由于一直围绕登录写代码所以还没写
public interface HostReplyDAO {
//根据reply的id获取HostReply
public HostReply getHostReplyByReplyId(Integer replyId);
}
实现类,这里其实提前预判到需要给Reply写带id的构造方法,因为HostReply的POJO属性写的是Reply类型,还需要借助我们的BaoDAO之前封装的反射模块去完成这个类型的封装
public class HostReplyDAOImpl extends BaseDao implements HostReplyDAO {
@Override
public HostReply getHostReplyByReplyId(Integer replyId) {
String sql = "SELECT * FROM t_host_reply WHERE reply = ?;";
List hostReplies = null;
try {
hostReplies = executeQuery(HostReply.class, sql, replyId);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("HostReplyDAOImpl.getHostReply出现异常!");
}
if(hostReplies != null && hostReplies.size() > 0) {
return hostReplies.get(0);
}
return null;
}
}
现在业务层的实现类就可以直接写好了
public class HostReplyServiceImpl implements HostReplyService {
private HostReplyDAO hostReplyDAO;
@Override
public HostReply getHostReplyByReplyId(Integer replyId) {
return hostReplyDAO.getHostReplyByReplyId(replyId);
}
}
现在获取对应Reply的全部HostReply功能已经没什么问题了,这里我们就能意识到之前获取全部Reply的Service实现类不应该直接返回replyDAO查询的所有方法,应该提前查一下有没有对应的HostReply,有的话把HostReply给它作为属性set进去
这里ReplyServiceImpl应该去调用HostReplyService实现类的方法,不要直接调用HostReplyDAO的,因为我们需要业务直接打交道,DAO方法是单精度的,我们调用业务层的方法一方面可以不去考虑内部实现的细节,第二方面我们是在给TopicController写内部实现的时候层层推进到这里,我们只想获取Reply的集合,而Reply怎么去获取对应的HostReply我不关心,和前面所说的思想类似,相应的类只取考虑自己分治的底层,和同层的别人的方法
public class ReplyServiceImpl implements ReplyService {
private ReplyDAO replyDAO;
private HostReplyService hostReplyService;
@Override
public List getReplyList(Integer topicId) {
List replyList = replyDAO.getReplyList(new Topic(topicId));
for (Reply reply : replyList) {
HostReply hostReply = hostReplyService.getHostReplyByReplyId(reply.getId());
if (hostReply != null) {
reply.setHostReply(hostReply);
}
}
return replyList;
}
}
那么我们意识到根据这种思想,我们不该在这个TopicController才实现把对应的Topic的所有关联的Reply作为属性关联的操作,这个应该在Service层就去完成,于是改写我们的TopicService层的实现类
public class TopicServiceImpl implements TopicService {
private TopicDAO topicDAO = null;
private ReplyService replyService = null;
@Override
public List getTopicList(UserBasic userBasic) {
return topicDAO.getTopicList(userBasic);
}
@Override
public Topic getTopicById(Integer id) {
Topic topic = topicDAO.getTopic(id);
List replyList = replyService.getReplyList(topic.getId());
topic.setReplyList(replyList);
return topic;
}
}
那么我们的TopicController完成这个操作的代码就可以去掉了
public class TopicController {
private TopicService topicService;
public String topicDetail(Integer id, HttpSession session){
Topic topic = topicService.getTopicById(id);
session.setAttribute("topic", topic);
return "frames/detail";
}
}
最后统一写到配置文件里面,这时我们就完成了目前的架构配置了,如果调试报空指针异常,那就是配置文件有地方写错了,可以进行修改。可以根据日志查询到对应的日志信息,日志的所有回复,所有回复对应的主人回复,前端部分还没有完成
]>
因为前端部分没完成,这里就打断点,看一下TopicController是否完成了查询所有信息,可以看到完成了所有信息的查询
接下来给我们的日志页面detail.html开始加入Thymeleaf,先给css的路径加入,然后给主页注入thymeleaf的标识,还有别的关于路径的,如图片的src都需要添加,但是图片这里数据库关于头像只存了一个图片的名称的字符串,所以这里肯定我们要用Thymeleaf的语法把session里面的信息给它。同时数据库图片名字和文件里面不一样,相应改一下。
同时还有删除小图标,标题,内容,都需要靠Thymeleaf语法写入src
这里意识到我们的topic对象在构造的时候其实内部外键的author是借助反射获取的只有id的用户,我们可以在业务层获取topiclist的时候,就提前调用UserBasicService的方法,生成列表的时候查询到用户的所有信息,然后再放入。当然这里用到了UserService的方法,势必要加入这个层的私有属性,并且写入Bean文件。
@Override
public Topic getTopicById(Integer id) {
Topic topic = topicDAO.getTopic(id);
UserBasic author = topic.getAuthor();
author = userBasicService.getUserBasicById(author.getId());
topic.setAuthor(author);
List replyList = replyService.getReplyList(topic.getId());
topic.setReplyList(replyList);
return topic;
}
这里老师把他们处理为两个单精度方法然后调用彼此。
//根据id获取指定的topic信息,包含这个topic关联的作者信息
public Topic getTopic(Integer id) {
Topic topic = topicDAO.getTopic(id);
UserBasic author = topic.getAuthor();
author = userBasicService.getUserBasicById(author.getId());
topic.setAuthor(author);
return topic;
}
//根据id获取topic信息,包含topic关联的回复信息
@Override
public Topic getTopicById(Integer id) {
Topic topic = getTopic(id);
List replyList = replyService.getReplyList(topic.getId());
topic.setReplyList(replyList);
return topic;
}
至此日志的信息搞定,接下来就是Reply的部分
同样,前端部分带有src和字段的地方要改写为Thymeleaf格式完成渲染,首先要在table部分迭代replyList,同时需要把回复的里面的属性渲染
这里意识到Reply里面的author按照之前DAO层的反射逻辑,肯定也只有id,我们这里也去给ReplyService业务层调用BasicService的方法把作者重新设置一下,同时注意别忘了把Bean属性注册在文件里面
public class ReplyServiceImpl implements ReplyService {
private ReplyDAO replyDAO;
private HostReplyService hostReplyService;
private UserBasicService userBasicService;
@Override
public List getReplyList(Integer topicId) {
List replyList = replyDAO.getReplyList(new Topic(topicId));
for (Reply reply : replyList) {
//1 设置作者
UserBasic author = userBasicService.getUserBasicById(reply.getAuthor().getId());
reply.setAuthor(author);
//2 设置主人回复
HostReply hostReply = hostReplyService.getHostReplyByReplyId(reply.getId());
if (hostReply != null) {
reply.setHostReply(hostReply);
}
}
return replyList;
}
}
这里由于我们加入了一个js函数,鼠标悬浮在上面才显示的功能,所以我们需要给主任回复的列表加入这个属性
帖子这里的回复部分和主人回复部分的前端代码当前版本如下
乔峰
《萧某今天就和天下群雄决一死战,你们一起上吧!》
2021-09-01 12:30:55
杀母大仇, 岂可当作买卖交易?此仇能报便报, 如不能报, 则我父子毕命于此便了。这等肮脏之事,
岂是我萧氏父子所屑为?
段誉
回复:萧某今天就和天下群雄决一死战,你们一起上吧!
2021-09-01 14:35:15
你可曾见过边关之上、宋辽相互仇杀的惨状?可曾见过宋人辽人妻离子散、家破人亡的情景?宋辽之间好容易罢兵数十年, 倘若刀兵再起, 契丹铁骑侵入南朝, 你可知将有多少宋人惨遭横死?多少辽人死于非命?!
-
你以为我是慕容复的人,所以和我比试?段兄弟年纪轻轻,就有如此武学修为,实属罕见!武林早已盛传大理段氏有一门绝学,叫六脉神剑,能以无形剑气杀人,果然真有此门神功!
-
2021/10/01 11:50:30
最终完成当前页面的添加回复的功能,下面是改造前
添加回复
首先添加回复,那我们的action应该是reply.do,我们应该创建ReplyController, 然后给它一个添加回复的方法,这里因为是表单,我们没法像之前一样直接带字符串键值对的形式,使用隐藏域的方式表示方法名
同时这里标题应该是这篇日志的标题,故直接用thymeleaf书写即可,现在我们来给Reply书写Controller,我们需要获取这个回复的哪个日志,需要一个id,这个id可以再书写隐藏域来获取到参数列表,同时我们需要从session获取一些信息,所以参数需要一个HttpSession,同时我们还从表单获取了content,参数列表还需要写一个content。
添加回复
再者我们是新建一个新的Reply,故我们需要通过构造方法去new,而时间我们可以靠Java当前时间获取,而id主键可以让数据库默认获取,HostReply可以从session获取,所以构造方法如下书写
当封装好一个新的Reply,发现我们还没有封装ReplyService的添加方法
故这里给Service层接口和实现类添加方法,同时把DAO层之前空着的方法补全
public interface ReplyService {
//根据topic的id获取全部的回复
public List getReplyList(Integer topicId);
//添加回复
public void addReply(Reply reply);
}
public class ReplyServiceImpl implements ReplyService {
private ReplyDAO replyDAO;
private HostReplyService hostReplyService;
private UserBasicService userBasicService;
@Override
public List getReplyList(Integer topicId) {
List replyList = replyDAO.getReplyList(new Topic(topicId));
for (Reply reply : replyList) {
//1 设置作者
UserBasic author = userBasicService.getUserBasicById(reply.getAuthor().getId());
reply.setAuthor(author);
//2 设置主人回复
HostReply hostReply = hostReplyService.getHostReplyByReplyId(reply.getId());
if (hostReply != null) {
reply.setHostReply(hostReply);
}
}
return replyList;
}
@Override
public void addReply(Reply reply) {
replyDAO.addReply(reply);
}
}
public interface ReplyDAO {
//获取指定日志的回复列表
public List getReplyList(Topic topic);
//添加回复
public void addReply(Reply reply);
//删除回复
public void delReply(Integer id);
}
public class ReplyDAOImpl extends BaseDao implements ReplyDAO {
@Override
public List getReplyList(Topic topic) {
String sql = "SELECT * FROM t_reply WHERE topic = ?;";
List replies = null;
try {
replies = executeQuery(Reply.class, sql, topic.getId());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("ReplyDAOImpl.getReplyList出现异常!");
}
return replies;
}
@Override
public void addReply(Reply reply) {
String sql = "INSERT INTO t_reply VALUES (0, ?, ?, ?, ?);";
try {
executeUpdate(sql, reply.getContent(), reply.getReplyDate(),
reply.getAuthor().getId(), reply.getTopic().getId());
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("ReplyDAOImpl.addReply出现异常!");
}
}
@Override
public void delReply(Integer id) {
String sql = "DELETE FROM t_reply WHERE id = ?;";
try {
executeUpdate(sql, id);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("ReplyDAOImpl.delReply出现异常!");
}
}
}
经过上面的增添,我们可以在Controller内部完成把这个reply添加到数据库的操作了,但是返回值是什么?我们直接跳转当前页面很显然,session作用域下的回复列表没有更新,只是数据库更新了,我们应该加入重定向的前缀,让我们查到最新的数据,同时千万别忘了注册
public class ReplyController {
private ReplyService replyService = null;
public String addReply(String content, Integer topicId, HttpSession session) {
UserBasic author = (UserBasic) session.getAttribute("userBasic");
Reply reply = new Reply(content, now(), author, new Topic(topicId));
replyService.addReply(reply);
return "redirect:topic.do?operate=topicDetail&id=" + topicId;
}
}
我们换一个用户进行评论操作,发现页面跳转失败,通过观看报错,发现content为空,仔细检查发现回复内容没有加name=content,没有提交名为content的数据
加上后,测试可以完成添加回复的功能。
接下来做删除回复和删除日志的功能。首先之前的删除图标这里的回复是迭代的,我们不可能id都相同,这里直接用thymeleaf把当前reply的id写在这里就行
同时我们的showDelImg也应该修改为相同格式
下面我们需要让这个删除图标在条件吻合的时候才显示,然后再做点击删除的功能。我们只有当前是我们自己的空间,或者这个回复是自己的回复的情况下才可以删除
为了防止找不到指定图片的情况下,前端报错,我们修改一下js的代码
这里我们先做回复列表的删除,我们添加一个js方法,当点击这个图片的时候调用,传入reply的id,然后这个js方法返回ReplyController去完成删除操作
然后去写ReplyController,我们之前写好了Dao层的删除,但是没给Service层的接口和实现类写方法,这里补上(图略)
但是我们删除之后想返回页面需要返回页面,所以需要topicId,所以js方法改写一下,然后传入的时候把topicId带上
此时Controller的代码如下
public class ReplyController {
private ReplyService replyService = null;
public String addReply(String content, Integer topicId, HttpSession session) {
UserBasic author = (UserBasic) session.getAttribute("userBasic");
Reply reply = new Reply(content, now(), author, new Topic(topicId));
replyService.addReply(reply);
return "redirect:topic.do?operate=topicDetail&id=" + topicId;
}
public String delReply(Integer replyId, Integer topicId) {
replyService.delReply(replyId);
return "redirect:topic.do?operate=topicDetail&id=" + topicId;
}
}
此时虽然能删除回复,但是发现删除有主人回复的回复,Mysql报错
删除回复
1) 如果回复有关联的主人回复,需要先删除主人回复
2) 删除回复
Cannot delete or update a parent row: a foreign key constraint fails
(`qqzonedb`.`t_host_reply`, CONSTRAINT `FK_host_reply` FOREIGN KEY (`reply`) REFERENCES `t_reply` (`id`))
我们在删除回复表记录时,发现删除失败,原因是:在主人回复表中仍然有记录引用待删除的回复这条记录
如果需要删除主表数据,需要首先删除子表数据
因此我们的删除reply的Service方法应该如下所示
先去补这个Dao方法
public interface ReplyDAO {
//获取指定日志的回复列表
public List getReplyList(Topic topic);
//添加回复
public void addReply(Reply reply);
//删除回复
public void delReply(Integer id);
//根据id获取回复
public Reply getReply(Integer id);
}
@Override
public Reply getReply(Integer id) {
String sql = "SELECT * FROM t_reply WHERE id = ?;";
List replies = null;
try {
replies = executeQuery(Reply.class, sql, id);
if (replies != null) {
return replies.get(0);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("ReplyDAOImpl.getReply出现异常!");
}
return null;
}
接着补充HostReply的删除
public interface HostReplyDAO {
//根据reply的id获取HostReply
public HostReply getHostReplyByReplyId(Integer replyId);
public void delHostReply(Integer id);
}
@Override
public void delHostReply(Integer id) {
String sql = "DELETE FROM t_host_reply WHERE id = ?;";
try {
executeUpdate(sql, id);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("HostReplyDAOImpl.deleteHostReply出错了");
}
}
public interface HostReplyService {
//获取一个Reply对应的全部HostReply
public HostReply getHostReplyByReplyId(Integer replyId);
void delHostReply(Integer id);
}
public class HostReplyServiceImpl implements HostReplyService {
private HostReplyDAO hostReplyDAO;
@Override
public HostReply getHostReplyByReplyId(Integer replyId) {
return hostReplyDAO.getHostReplyByReplyId(replyId);
}
@Override
public void delHostReply(Integer id) {
hostReplyDAO.delHostReply(id);
}
}
故我们的Reply的Service层的删除方法为:
@Override
public void delReply(Integer id) {
//1.根据id获取到reply
Reply reply = replyDAO.getReply(id);
if (reply != null) {
//2.如果reply有关联的hostReply,则先删除hostReply
if(reply.getHostReply()!= null){
hostReplyService.delHostReply(reply.getHostReply().getId());
}
//3.删除reply
replyDAO.delReply(id);
}
}
现在删除Reply的功能彻底做好了,测试发现还不行,仔细排查发现HostReply的值为空,突然意识到从Dao层获取的reply,根本没有HostReply的属性,所以get方法根本拿不到它
我们应该利用Reply的id从HostReply的DAO层去获取有无对应的Hostreply
@Override
public void delReply(Integer id) {
//1.根据id获取到reply
Reply reply = replyDAO.getReply(id);
if (reply != null) {
//2.如果reply有关联的hostReply,则先删除hostReply
HostReply hostReply = hostReplyService.getHostReplyByReplyId(id);
if(hostReply!= null){
hostReplyService.delHostReply(hostReply.getId());
}
//3.删除reply
replyDAO.delReply(id);
}
}
按同样的思路,我们的删除日志功能也应该有相同的逻辑
1) 删除日志,首先需要考虑是否有关联的回复
2) 删除回复,首先需要考虑是否有关联的主人回复
3) 另外,如果不是自己的空间,则不能删除日志
然后我们给它添加一个js方法,当被点击的时候能够删除这个topic
因此我们需要TopicController去写对应的方法,先给DAO和Sercvice层写
public interface TopicService {
public List getTopicList(UserBasic userBasic);
//根据id获取topic信息,包含topic关联的回复信息
public Topic getTopicById(Integer id);
//根据id获取指定的topic信息,包含这个topic关联的作者信息
public Topic getTopic(Integer id);
//根据id删除指定的topic信息,包含这个topic关联的所有
public void delTopic(Integer id);
}
实现类的书写
@Override
public void delTopic(Integer id) {
Topic topic = getTopic(id);
if (topic != null){
replyService.delReplyList(topic);
topicDAO.delTopic(topic);
}
}
dao的方法之前没实现,现在实现一下
public interface TopicDAO {
// 根据用户获取所有日志列表
public List getTopicList(UserBasic userBasic);
// 添加日志
public void addTopic(Topic topic);
// 删除日志
public void delTopic(Topic topic);
// 获取特定日志信息
public Topic getTopic(Integer id);
}
dao实现类
@Override
public void delTopic(Topic topic) {
String sql = "DELETE FROM t_topic WHERE id = ?;";
try {
executeUpdate(sql, topic.getId());
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("TopicDAOImpl.deleteTopic出现异常");
}
}
同时删除一个日志需要把他关联的所有回复都删除,所以我们不妨从ReplyService写相关的方法
public interface ReplyService {
//根据topic的id获取全部的回复
public List getReplyList(Integer topicId);
//添加回复
public void addReply(Reply reply);
//删除回复
public void delReply(Integer topicId);
//删除指定日志关联的所有回复
public void delReplyList(Topic topic);
}
实现类,这里调用我们之前在ReplyService已经写了的删除方法,也会把主人回复删除
@Override
public void delReplyList(Topic topic) {
List replyList = replyDAO.getReplyList(topic);
if (replyList != null) {
for (Reply reply : replyList) {
delReply(reply.getId());
}
}
}
@Override
public void delReply(Integer id) {
//1.根据id获取到reply
Reply reply = replyDAO.getReply(id);
if (reply != null) {
//2.如果reply有关联的hostReply,则先删除hostReply
HostReply hostReply = hostReplyService.getHostReplyByReplyId(id);
if(hostReply!= null){
hostReplyService.delHostReply(hostReply.getId());
}
//3.删除reply
replyDAO.delReply(id);
}
}
这里我们发现返回页面frames/main,是从sssion中的friend获取的,而数据更新需要重新把查完的信息再覆盖到session,故重写
public class TopicController {
private TopicService topicService;
public String topicDetail(Integer id, HttpSession session){
Topic topic = topicService.getTopicById(id);
session.setAttribute("topic", topic);
return "frames/detail";
}
public String delTopic(Integer id){
topicService.delTopic(id);
return "redirect:topic.do?operate=getTopicList";
}
public String getTopicList(HttpSession session){
//从session中获取当前用户信息
UserBasic userBasic = (UserBasic)session.getAttribute("userBasic");
//再次查询当前用户关联的所有的日志
List topicList = topicService.getTopicList(userBasic);
//设置一下关联的日志列表(因为之前session中关联的friend的topicList和此刻数据库中不一致)
userBasic.setTopicList(topicList);
//重新覆盖一下friend中的信息(为什么不覆盖userbasic中?因为main.html页面迭代的是friend这个key中的数据)
session.setAttribute("friend",userBasic);
return "frames/main";
}
}
别忘了注入文件到xml(事实上应该不需要注入,没有添加额外的关联)
http://localhost:8080/pro23/page.do?operate=page&page=login 访问这个URL,执行的过程是什么样的?
答:http:// localhost :8080 /pro23 /page.do ?operate=page&page=login 协议 ServerIP port context root request.getServletPath() queryString
1) DispatcherServlet -> urlPattern : *.do 拦截/page.do
2) request.getServletPath() -> /page.do
3) 解析处理字符串,将/page.do -> page
4) 拿到page这个字符串,然后去IOC容器(BeanFactory)中寻找id=page的那个bean对象 -> PageController.java
5) 获取operate的值 -> page 因此得知,应该执行 PageController中的page()方法
6) PageController中的page方法定义如下:
public String page(String page){
return page ;
}
7) 在queryString: ?operate=page&page=login 中 获取请求参数,参数名是page,参数值是login
因此page方法的参数page值会被赋上"login"
然后return "login" , return 给 谁??
8) 因为PageController的page方法是DispatcherServlet通过反射调用的
method.invoke(....) ;
因此,字符串"login"返回给DispatcherServlet
9) DispatcherServlet接收到返回值,然后处理视图
目前处理视图的方式有两种: 1.带前缀redirect: 2.不带前缀
当前,返回"login",不带前缀
那么执行 super.processTemplete("login",request,response);
10) 此时ViewBaseServlet中的processTemplete方法会执行,效果是:
在"login"这个字符串前面拼接 "/" (其实就是配置文件中view-prefixe配置的值)
在"login"这个字符串后面拼接 ".html" (其实就是配置文件中view-suffix配置的值)
最后进行服务器转发
1. 拷贝 myssm包
2. 新建配置文件applicationContext.xml或者可以不叫这个名字,在web.xml中指定文件名
3. 在web.xml文件中配置:
1) 配置前缀和后缀,这样thymeleaf引擎就可以根据我们返回的字符串进行拼接,再跳转
2) 配置监听器要读取的参数
目的是加载IOC容器的配置文件(也就是applicationContext.xml)
4. 开发具体的业务模块:
1) 一个具体的业务模块纵向上由几个部分组成:
- html页面
- POJO类
- DAO接口和实现类
- Service接口和实现类
- Controller 控制器组件
2) 如果html页面有thymeleaf表达式,一定不能够直接访问,必须要经过PageController
3) 在applicationContext.xml中配置 DAO、Service、Controller,以及三者之间的依赖关系
4) DAO实现类中 , 继承BaseDAO,然后实现具体的接口
例如:
public class UserDAOImpl extends BaseDAO implements UserDAO{}
5) Service是业务控制类,这一层我们只需要记住一点:
- 业务逻辑我们都封装在service这一层,不要分散在Controller层。也不要出现在DAO层(我们需要保证DAO方法的单精度特性)
- 当某一个业务功能需要使用其他模块的业务功能时,尽量的调用别人的service,而不是深入到其他模块的DAO细节
6) Controller类的编写规则
① 在applicationContext.xml中配置Controller
因此,我们的登录验证的表单如下:
③ 在表单中,组件的name属性和Controller中方法的参数名一致
public String login(String loginId , String pwd , HttpSession session){
④ 另外,需要注意的是: Controller中的方法中的参数不一定都是通过请求参数获取的
if("request".equals...) else if("response".equals....) else if("session".equals....){
直接赋值
}else{
此处才是从request的请求参数中获取
request.getParameter("loginId") .....
}
7) DispatcherServlet中步骤大致分为:
0. 从application作用域获取IOC容器
1. 解析servletPath , 在IOC容器中寻找对应的Controller组件
2. 准备operate指定的方法所要求的参数
3. 调用operate指定的方法
4. 接收到执行operate指定的方法的返回值,对返回值进行处理 - 视图处理
8) 为什么DispatcherServlet能够从application作用域获取到IOC容器?
ContextLoaderListener在容器启动时会执行初始化任务,而它的操作就是:
1. 解析IOC的配置文件,创建一个一个的组件,并完成组件之间依赖关系的注入
2. 将IOC容器保存到application作用域
6. 修改BaseDAO,让其支持properties文件以及druid数据源连接池
讲解了两种方式:
1) 直接自己配置properties,然后读取,然后加载驱动.....
2) 使用druid连接池技术,那么properties中的key是有要求的