Maven项目之黑马旅游网

视频讲解

本项目是根据黑马程序员的教学视频完成。完成一个黑马旅游网相关功能的实现。

这里就记录一些自己在完成项目时遇到的问题以及解决方法

代码所在仓库地址:https://gitee.com/qingyu1011/springboot_study/tree/master/Maven/Travel03

项目初始化

数据库准备

相关sql语句在上述仓库中的resources文件下,创建后的结果为:

数据库相关表

创建Maven webapp项目

视频讲解

这里我们选择使用骨架的方式

第一步:选择骨架

第二步:这里的配置就很熟悉了,不用多说

第三步

之后Finish即可

通过骨架创建好webapp项目后,我们删除pom.xml中的一些内容,只保留剩余代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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>top.qing</groupId>
<artifactId>Travel03</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<name>Travel03 Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>


</project>

这个时候的目录结构如下:

这个时候需要我们手动加入一些文件目录(如java、test)。步骤如下:

新版IDEA非常智能

技术选型

Web层

  • Servlet:前端控制器
  • html:视图
  • Filter:过滤器
  • BeanUtils:数据封装
  • Jackson:json序列化工具

Service层

  • Javamail:java发送邮件工具
  • Redis:nosql内存数据库
  • Jedis:java的redis客户端

Dao层

  • Mysql:数据库
  • Druid:数据库连接池
  • JdbcTemplate:jdbc的工具

代码实现

注册功能

视频讲解

分析:

注册功能分析

表单校验

表单检验:提升用户体验,并减轻服务器压力

利用正则表达式,在注册时要进行表单校验:

  1. 用户名:单词字符,长度8到20位
  2. 密码:单词字符,长度8到20位
  3. email:邮件格式
  4. 姓名:非空
  5. 手机号:手机号格式
  6. 出生日期:非空
  7. 验证码:非空

视频讲解

(代码和异步(ajax)提交表单一起放在下面)

异步(ajax)提交表单

在此使用异步提交表单是为了获取服务器响应的数据。因为我们前台使用的是html作为视图层,不能够直接从servlet相关的域对象获取值,只能通过ajax获取响应数据。

视频讲解

表单检验和异步提交代码如下:

在regist.html的<head>标签下放入代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
    <!--导入jquery-->
<script src="js/jquery-3.3.1.js"></script>
<script>
/*
表单校验:
1.用户名:单词字符,长度8到20位
2.密码:单词字符,长度8到20位
3.email:邮件格式
4.姓名:非空
5.手机号:手机号格式
6.出生日期:非空
7.验证码:非空
*/
//校验用户名
//单词字符,长度8到20位
function checkUsername() {
//1.获取用户名值
var username = $("#username").val();
//2.定义正则
var reg_username = /^\w{8,20}$/;

//3.判断,给出提示信息
var flag = reg_username.test(username);
if(flag){
//用户名合法
$("#username").css("border","");
}else{
//用户名非法,加一个红色边框
$("#username").css("border","1px solid red");
}

return flag;
}

//校验密码
function checkPassword() {
//1.获取密码值
var password = $("#password").val();
//2.定义正则
var reg_password = /^\w{8,20}$/;

//3.判断,给出提示信息
var flag = reg_password.test(password);
if(flag){
//密码合法
$("#password").css("border","");
}else{
//密码非法,加一个红色边框
$("#password").css("border","1px solid red");
}

return flag;
}

//校验邮箱
function checkEmail(){
//1.获取邮箱
var email = $("#email").val();
//2.定义正则 qingbo1011@163.com
var reg_email = /^\w+@\w+\.\w+$/;

//3.判断
var flag = reg_email.test(email);
if(flag){
$("#email").css("border","");
}else{
$("#email").css("border","1px solid red");
}
return flag;
}

//校验姓名
function checkName(){
//1.获取姓名
var name = $("#name").val();
//2.定义正则 非空
var reg_name = /^[\s\S]*.*[^\s][\s\S]*$/;

//3.判断
var flag = reg_name.test(name);
if(flag){
$("#name").css("border","");
}else{
$("#name").css("border","1px solid red");
}
return flag;
}

//校验手机号
function checkTelephone(){
//1.获取手机号
var telephone = $("#telephone").val();
//2.定义正则 手机号的正则
var reg_telephone = /^1[3|4|5|7|8][0-9]{9}$/;

//3.判断
var flag = reg_telephone.test(telephone);
if(flag){
$("#telephone").css("border","");
}else{
$("#telephone").css("border","1px solid red");
}
return flag;
}

//校验出生日期
function checkBirthday(){
//1.获取出生日期
var birthday = $("#birthday").val();
//2.定义正则 非空
var reg_birthday = /^[\s\S]*.*[^\s][\s\S]*$/;

//3.判断
var flag = reg_birthday.test(birthday);
if(flag){
$("#birthday").css("border","");
}else{
$("#birthday").css("border","1px solid red");
}
return flag;
}

//校验验证码
function checkCheckCode(){
//1.获取验证码
var check = $("#check").val();
//2.定义正则 非空
var reg_check = /^[\s\S]*.*[^\s][\s\S]*$/;

//3.判断
var flag = reg_check.test(check);
if(flag){
$("#check").css("border","");
}else{
$("#check").css("border","1px solid red");
}
return flag;
}

//当表单提交时,调用所有的校验方法
$(function () {
$("#registerForm").submit(function(){
//1.发送数据到服务器
if(checkUsername() && checkPassword() && checkEmail() && checkName() && checkTelephone() && checkBirthday() && checkCheckCode()){
//校验通过,发送ajax请求,提交表单的数据 username=zhangsan&password=123
$.post("registUserServlet",$(this).serialize(),function(data){
//处理服务器响应的数据 data {flag:true,errorMsg:"注册失败"}
if(data.flag){
//注册成功,跳转成功页面
location.href="register_ok.html";
}else{
//注册失败,给errorMsg添加提示信息
$("#errorMsg").html(data.errorMsg);
}
});
}
//2.不让页面跳转
return false;
//如果这个方法没有返回值,或者返回为true,则表单提交,如果返回为false,则表单不提交
});
//当某一个组件失去焦点是,调用对应的校验方法
$("#username").blur(checkUsername);
$("#password").blur(checkPassword);
$("#email").blur(checkEmail);
$("#name").blur(checkName);
$("#telephone").blur(checkTelephone);
$("#birthday").blur(checkBirthday);
$("#check").blur(checkCheckCode);
});

</script>

注册功能后端代码

视频讲解

  • 编写RegistUserServlet

  • 编写UserService接口以及UserServiceImpl

  • 编写UserDao接口以及UserDaoImpl

这一块具体就看仓库代码吧。不过写的时候要细心,不然很容易报空指针异常。

邮件激活功能

为什么要进行邮件激活?为了保证用户填写的邮箱是正确的。将来可以推广一些宣传信息,到用户邮箱中。

关于邮件激活动能就不添加了,但是还是可以了解一下。

视频讲解

登录功能

分析:

登录功能

视频讲解

前端代码

  • login.hyml
  • header.html

后端代码

  • 编写LoginUserServlet
  • 补充UserService接口以及UserServiceImpl
  • 补充UserDao接口以及UserDaoImpl

退出功能

什么叫做登录了?session中有user对象。

实现步骤:

  1. 访问servlet,将session销毁
  2. 跳转到登录页面

BaseServlet抽取

减少Servlet的数量,现在是一个功能一个Servlet,将其优化为一个模块一个Servlet,相当于在数据库中一张表对应一个Servlet,在Servlet中提供不同的方法,完成用户的请求。

视频讲解

例子:创建BaseServlet如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package top.qing.web.servlet;

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.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class BaseServlet extends HttpServlet {

/**
* 由于UserServlet继承了BaseServlet,所以当/user/*被访问时就会执行BaseServlet中的service方法
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 完成方法的分发
//1.获取请求路径
String uri = req.getRequestURI(); // 例如/travel/user/add

//2.获取方法名称
// substring方法和lastIndexOf方法可自行查看。该语句的作用为:截取最后一个'/'字符后的字符串
String methodName = uri.substring(uri.lastIndexOf('/') + 1); // 结果为add

//3.获取方法对象Method
// 这里的this:谁调用service,this就代表谁(如:如果是UserServlet调用,那么this就代表UserServlet)
try {
// getDeclaredMethod(忽略方法的访问权限修饰符。具体可看反射笔记)
Method method = this.getClass().getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
// 执行方法
method.invoke(this,req,resp);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}


}
}

抽取UserServlet代码见仓库

分类数据展示

效果:

分析:

分类数据查询

视频讲解

后端代码

  • CategoryServlet
  • CategoryService
  • CategoryDao

前端代码

  • header.html

缓存优化:redis

分析发现,分类的数据在每一次页面加载后都会重新请求数据库来加载,对数据库的压力比较大,而且分类的数据不会经常产生变化,所有可以使用redis来缓存这个数据。

优化,以后就不需要每次都发送findAll请求了

旅游线路的分页展示

点击了不同的分类后,将来看到的旅游线路不一样的。通过分析数据库表结构,发现,旅游线路表和分类表时一个多对一的关系

根据id查询不同类别的旅游线路数据(分页展示

分页展示旅游线路数据

分析:

分页查询

视频讲解

客户端(前端)代码编写

  • 客户端发送ajax请求,请求PageBean数据
  • 携带三个参数:currentPage,pageSize,cid

服务器端(后端)代码编写

  • 创建PageBean对象
  • RouteServlet
  • RouteService
  • RouteDao

创建PageBean对象代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package top.qing.domain;

import lombok.Data;

import java.util.List;

/**
*分页对象
*/
@Data
public class PageBean<T> {
private Integer totalCount; //总记录数
private Integer totalPage; //总页数
private Integer currentPage; //当前页码
private Integer pageSize; //每页显示的条数

private List<T> list; //每页显示的数据集合
}

旅游线路名称查询

前端代码

  • 查询参数的传递

后端代码

  • 修改RouteServlet
  • 修改RouteService
  • 修改RouteDao

(都是补充rname参数,这个rname就是从前端传来的用户查询的关键字)

注意一下RouteDaoImpl的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package top.qing.dao.impl;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import top.qing.dao.RouteDao;
import top.qing.domain.Route;
import top.qing.util.JDBCUtils;

import java.util.ArrayList;
import java.util.List;

public class RouteDaoImpl implements RouteDao {
private JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

@Override
public int findTotalCount(int cid,String rname) {
// String sql = "select count(rid) from tab_route where cid = ?";
// 1.定义sql模板
String sql = "select count(rid) from tab_route where 1=1";
StringBuilder stringBuilder = new StringBuilder(sql);
List params = new ArrayList<>(); // 将条件(即占位符?代表的内容,按顺序存放到params中)
// 2.判断参数(cid和rname)是否为null
if (cid!=0){
stringBuilder.append(" and cid = ?");
params.add(cid); // 添加占位符?代表的cid
}
if (rname!=null&&rname.length()>0){
stringBuilder.append(" and rname like ?");
params.add("%"+rname+"%"); // 添加占位符?代表的%rname%
}
sql = stringBuilder.toString();
// params.toArray(),将List转为数组,作为queryForObject方法的参数
return jdbcTemplate.queryForObject(sql, Integer.class, params.toArray());
}

@Override
public List<Route> findByPage(int cid, int start, int pageSize,String rname) {
// String sql = "select * from tab_route where cid = ? limit ?,?";
// 1.定义sql模板
String sql = "select * from tab_route where 1=1";
StringBuilder stringBuilder = new StringBuilder(sql);
List params = new ArrayList<>(); // 将条件(即占位符?代表的内容,按顺序存放到params中)
// 2.判断参数(cid和rname)是否为null
if (cid!=0){
stringBuilder.append(" and cid = ?");
params.add(cid); // 添加占位符?代表的cid
}
if (rname!=null&&rname.length()>0){
stringBuilder.append(" and rname like ?");
params.add("%"+rname+"%"); // 添加占位符?代表的%rname%
}
stringBuilder.append(" limit ?.,?"); // 分页查询
params.add(start); // 开始的索引
params.add(pageSize); // 每页查询的条数
sql = stringBuilder.toString();
List<Route> routes = jdbcTemplate.query(sql, new BeanPropertyRowMapper<Route>(Route.class), params.toArray());
return routes;
}
}

查看旅游详情

分析:

视频讲解

前端代码

Route_detail.html中加载后:

  1. 获取rid
  2. 发送ajax请求,获取route对象
  3. 解析对象的数据

后端代码

  • 补充RouteServlet:增加findOne()方法,用于查询一个旅游线路的详细信息(即一个Route对象)
  • 补充RouteService
  • 补充RouteDao

旅游线路收藏展示

分析:

当页面加载完成后,发送ajax请求,获取用户是否收藏的标记。根据标记,展示不同的按钮样式

视频讲解

前端代码

  • route_detail.html

后端代码

  • 补充RouteServlet:添加isFavorite()方法,用于判断该用户是否收藏了线路

  • FavoriteService

  • FavoriteDao

  • 在RouteServiceImpl中的findOne()方法上,通过tab_favorite表查询路径的收藏数量count

点击按钮收藏路线功能

分析:

视频讲解

前端代码

route_detail.html

后端代码

  • 补充RouteServlet:addFavorite()方法,用于更新tab_favorite表的数据,从逻辑上完成收藏功能
  • 补充FavoriteService
  • 补充FavoriteDao

遇到的问题

pom.xml之lombok

最开始我选择的lombok版本为1.18.12版的,在tomcat运行后会报错:

Unable to process Jar entry [module-info.class] from Jar.....for annotations

解决方案:将lombok版本降至1.16.18版本

参考文章

空指针异常

是由于写代码粗心导致的。比如说在UserServiceImpl实现类中的regist方法,本来应该保存user(参数由前端数据提供并封装的),但我却错误的写成了user_find(从数据库查询的用于判断用户名是否存在)。

BaseServlet中的一些方法封装

响应数据writeValue方法:

1
2
3
4
5
6
7
8
// 将info对象序列化为json格式的字符串(使用Jackson)
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(resultInfo);

// 先设置响应头Content-Type:服务器告诉客户端本次响应体数据格式以及编码格式
response.setContentType("application/json;charset=utf-8");
// 将json数据写回客户端(设置响应体,将数据输出到客户端浏览器)
response.getWriter().write(json);