项目代码来自楠哥的Javaweb视频结尾最后那个练习工程,作为萌新,我跟着敲了一遍又整理了一遍后有了一些自己的浅薄理解,现在把它分享出来,供大家学习交流使用。希望能帮助到跟我一样刚刚入门且恰巧在看楠哥课程的萌新,如有不对的地方欢迎大家批评指正。传送门放在下面。
【白送资料】有料有深度的javaweb教程,tomcat10讲解_哔哩哔哩_bilibili
我们写一个用户管理模块实现对用户的增删改查。
大概实现以下功能即可:
1、登录注册,验证码、动态判断用户是否存在;
2、用户的增删改查,多表的联查;
3、用户同步、异步的分页展示的实现。
了解并使用tomcat工具,学习项目中的功能分层思想、MVC框架、学习项目的打包部署等等,学会看技术文档的习惯,很多东西谁都记不住,但是文档哪块都有,一定记住遇到不会的先找文档,能解决大部分问题,一定要养成看文档解决问题的习惯!
根据MVC架构将代码创建创建好,包括以下步骤:
配置完毕如图所示:
我们首先要写一个register注册页面(jsp文件),这个时候需要使用我们的Bootstarp工具来帮助我们写html注册页面。按照文档中的要求去进行配置。
在全局css样式中找到符合我自己需求的表单,cv就完事了!
目前效果还可以,但是位置不舒服,需要用到css中的栅格系统进行调整。
用一行三列的模式去调整:
.col-md-4
.col-md-4
.col-md-4
最终成品如下:
register.jsp如下所示:
<%--
User: Administrator
Date: 2023/4/11
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
Title
用户管理系统
现在我们就有了目标,我们需要写一个user类来存储用户对象,需要写servlet实现注册功能,需要UserDao类来跟数据库打交道,接下来我们一个一个的写。
我们写一个User类:后面都是构造方法等,省略
public class User implements Serializable {
private static final Long serialVersionUID = 1L;
private Integer id;
private String username;
private String password;
private Integer deptId;
.......
然后写我们的Servlet来实现注册,即写MVC中的C——控制层。
@WebServlet("/user/register")
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
User user = new User(username, password);
userService.register(user);
}
}
我们的控制层相当于M(模型)和V(视图)之间的传输信息的渠道,只用把信息进行封装就好了,封装完成后扔给UserService去处理(M)。我们接下来写UserService。
public class UserService {
UserDao userDao = new UserDao();
// 执行业务逻辑
public void register(User user) {
// 实质是向数据库插入一条数据
userDao.save(user);
}
}
我们发现我们要处理的业务实质就是在数据库中增加一条数据。一旦涉及到数据库的操作,就都需要我们Dao出手,这就需要我们创建UserDao这个类,调用它自己的save方法,给数据库中存放数据。接下来我们写UserDao。
我们先采用JDNI来连接数据源,根据下面操作引入context.xml文件。
我们来通过JDNI配置数据源(context.xml):
<Context path="/">
<Resource name="dataSource/mysql/"
auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://127.0.0.1:3306/ydl?characterEncoding=utf8&serverTimezone=Asia/Shanghai"
username="root" password="285738"
maxTotal="20" maxIdle="10"
maxWaitMillis="10000" />
Context>
但是又有问题,我们要写UserDao想跟数据库打交道,要用到JDNI来配置数据源和断开数据源的链接,但是后续我们不止有一个Dao,可能还有其他的Dao都要用JDNI来配置数据源和断开数据源的链接。这个重复的操作没有必要反复写,于是我们抽象出来一个BaseDao专门来实现使用JDNI来配置数据源的操作。后面如果有别的Dao的话统一来继承这个BaseDao就好了。
public class BaseDao {
protected Connection getConn() {
try {
Context context = new InitialContext();
DataSource dataSource = (DataSource) context.lookup("java:comp/env/dataSource/mysql/prod");
return dataSource.getConnection();
} catch (NamingException | SQLException e) {
e.printStackTrace();
}
return null;
}
// 断开数据源的操作
protected void closeAll(Connection connection, Statement statement, ResultSet resultSet) {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
有了BaseDao,我们接下来使用UserDao继承它来完成我们save的业务逻辑。
public class UserDao extends BaseDao {
// 保存用户
public int save(User user) {
String sql = "insert into user(username,password) values (?, ?)";
PreparedStatement statement = null;
Connection conn = getConn();
try {
statement = conn.prepareStatement(sql);
statement.setString(1, user.getUsername());
statement.setString(2, user.getPassword());
// 受影响的行数
return statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, null);
}
return 0;
}
}
OK,目前为止,我们已经实现了一个最基本的用户注册的功能!
我们输入的密码在数据库中被一览无余,任何运维人员都可以随意看到我们的密码吗?这显然是不现实的,我们希望通过某种技术将密码处理成一个大家都看不懂的形式存储起来,当登陆等需要数据库的密码时,我们通过某种这种算法将密码处理后再跟数据库的密码进行比对。
我们采取MD5加密,相当于使用哈希散列算法对密码进行处理,这样就实现了所有能看到数据库的人也不知道我们的密码到底是多少。
介绍:MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5由美国密码学家【罗纳德·李维斯特】设计,于1992年公开,用以取代MD4算法。1996年后该算法被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法,如SHA-2。
public static void main(String[] args) throws Exception { MessageDigest md5 = MessageDigest.getInstance("MD5"); byte[] digest = md5.digest("123".getBytes()); System.out.println(Arrays.toString(digest)); } [32, 44, -71, 98, -84, 89, 7, 91, -106, 75, 7, 21, 45, 35, 75, 112]
我们建立一个工具类MD5Util来专门处理我们的密码加密问题。
public class MD5Util {
public static String digest(String content) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
byte[] digest = md5.digest(content.getBytes());
return new String(digest);
}
}
再更改一下我们的UserService,再给UserDao执行存入数据操作前先将密码加密,之后再存入数据库。
public class UserService {
UserDao userDao = new UserDao();
// 执行业务逻辑
public void register(User user) {
// 将密码进行MD5加密运算后再存入数据库
user.setPassword(MD5Util.digest(user.getPassword()));
// 实质是向数据库插入一条数据
userDao.save(user);
}
}
这样我们就实现了加密存储,我们存储了一组数据"123;123"在数据库中看password只能看到乱码
但是这样子不美观,看的全是乱码形式,我们为了美观着想,再加一层Base64编码在外面,使它变成字母和符号组合的形式。我们来更改MD5Util来实现这个功能:
public class MD5Util {
public static String digest(String content) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
byte[] digest = md5.digest(content.getBytes());
// 为了美观着想,不让数据库中的密码以乱码形式显示出来,再加一层base64的编码。
Base64.Encoder encoder = Base64.getEncoder();
byte[] encode = encoder.encode(digest);
return new String(encode);
}
}
我们有一些用户就会使用非常简单的密码,为了他的安全着想,我们可以采取给他取的密码加上一部分内容后再进行加密的操作来增强用户密码的复杂性。
加的内容多种多样,根据自己的需要来定,例如可以给数据库表中再加一栏随机数,密码加密前读取这个用户对应的这个随机数给他加到密码后再进行加密,最后再存进数据库中;或者也可以直接使用他自己的用户名来进行加盐,最后再加密等等。
我们采取username+password+盐
的形式来对密码进行加盐处理。盐我们采取一个统一的常量定义。新建常量目录和常量类来定义我们的盐:
package com.ydlclass.constant;
public class Constant {
public static final String SALT = "!@#hjjkh!H!JKH";
}
更改UserService类,让他的执行逻辑是先加盐再加密:
public class UserService {
UserDao userDao = new UserDao();
// 执行业务逻辑
public void register(User user) {
// 设计密码(加盐)
String password = user.getUsername() + user.getPassword() + Constant.SALT;
// 将密码进行MD5加密运算后再存入数据库(加密)
user.setPassword(MD5Util.digest(password));
// 实质是向数据库插入一条数据
userDao.save(user);
}
}
现在,我们就基本实现了密码的加盐和加密问题,大大增强了用户们的密码安全性!(下图是输入账号admin密码123的用户注册后数据库能看到的信息)
我们想要实现头像的上传功能(上传文件)。
我们首先要改变register.jsp中的表单发送Post请求的传输方式,改为多部分的传输
才能传输文件。我们把其中的form表头加上enctype属性:
原先我们的Content-Type为application/x-www-form-urlencoded,改变表头后变为multipart/form-data,后面跟着一堆二进制数据,这样就改变了Post请求的传输方式,就可以通过流的方式来解析我们上传的图片了。
我们需要更改相应的UserContorller类的代码,让他能够读取文件流写入到我们的指定目录中,达到存储图片的目的。
@WebServlet("/user/register")
@MultipartConfig
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
// 不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是这个多部分的传输格式,通过注解的方式(@MultipartConfig)告诉servlet。
InputStream inputStream = null;
OutputStream outputStream = null;
try {
Part profile = req.getPart("profile");
inputStream = profile.getInputStream();
outputStream = new FileOutputStream("E://Java/Projects/learnTomcat/user-manager/www/img/" + profile.getSubmittedFileName());
byte[] buf = new byte[1024 * 1024];
int len;
while ((len = inputStream.read(buf)) != -1) {
outputStream.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
User user = new User(username, password);
userService.register(user);
}
}
现在我们已经实现了,通过网页传输文件到我们指定文件夹的功能了。
我们在一个UserContorller类中写一个存储文件这个方法显然是不合适的,我们如果后面还有其他类需要又得再写一遍,我们需要把这个方法抽离出去,这个思想很重要!!我们写一个IOUtil工具类,用来实现文件拷贝的功能:
/**
* 实现拷贝上传文件到具体目录的功能
*/
public class IOUtil {
public static void copy(InputStream inputStream, String path) {
OutputStream outputStream = null;
try {
outputStream = new FileOutputStream(path);
byte[] buf = new byte[1024 * 1024];
int len;
while ((len = inputStream.read(buf)) != -1) {
outputStream.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
我们通过构建了工具类抽离出去了拷贝文件的功能,这样就可以更方便的完善我们的功能了,加入没传头像的判断等等。UserContorller类更新后的代码如下:
@WebServlet("/user/register")
@MultipartConfig
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
// 用于存放文件的部分
Part profile = null;
InputStream inputStream = null;
// 获取文件的部分不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是多部分的传输格式(通过注解的方式(@MultipartConfig)告诉servlet)。
try {
profile = req.getPart("profile");
inputStream = profile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (profile == null || inputStream == null) {
throw new RuntimeException("您必须上传头像!");
}
// 获取文件名
String fileName = profile.getSubmittedFileName();
// 通过自己定义的工具类copy文件
IOUtil.copy(inputStream, Constant.BASEPATH + fileName);
User user = new User(username, password);
userService.register(user);
}
}
其中Constant.BASEPATH是我们定义的文件存放路径。我们用于储存图片的根路径也是一个常量,所以也直接定义在常量类中了,直接调用就行。
public class Constant {
// 加密用的盐常量
public static final String SALT = "!@#hjjkh!H!JKH";
// 传图片存图片的根路径
public static final String BASEPATH = "E://Java/Projects/learnTomcat/user-manager/www/img/";
}
这样我们就更新好了我们的代码,剥离出了一个IOUtil类来实现文件拷贝的功能。
我们又发现一个问题,如果两个用户上传的是同一个名字的图片,就会造成图片覆盖的情况,这是我们不希望发生的,丢失了一个用户的头像,我们应该怎么改进呢?
我们可以给名字前增加UUID,来使不同用户上传的文件名不可能重复。
@WebServlet("/user/register")
@MultipartConfig
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
// 用于存放文件的部分
Part profile = null;
InputStream inputStream = null;
// 获取文件的部分不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是多部分的传输格式(通过注解的方式(@MultipartConfig)告诉servlet)。
try {
profile = req.getPart("profile");
inputStream = profile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (profile == null || inputStream == null) {
throw new RuntimeException("您必须上传头像!");
}
// 需要使用UUID进行处理,使得文件名不可能重复,防止覆盖掉用户头像信息
String fileName = UUID.randomUUID().toString() + "_" + profile.getSubmittedFileName();
// 通过自己定义的工具类copy文件
IOUtil.copy(inputStream, Constant.BASEPATH + fileName);
User user = new User(username, password);
userService.register(user);
}
}
至于为什么要增加"_"
,这么做的目的是后续如果有需要使用文件名的话可以通过spilt(“_”)方法来分离UUID,得到真实的文件名。
我们觉得一直存在一个文件夹里的话文件太大了,有需求建立不同文件夹来存取。
这个功能的实现也很简单,只需要在文件夹路径前调出当前日期加进去创建一个文件夹就好了,需要使用文件流进行判断是否存在这个文件夹,不存在进行创建。更新代码如下:
@WebServlet("/user/register")
@MultipartConfig
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
// 用于存放文件的部分
Part profile = null;
InputStream inputStream = null;
// 获取文件的部分不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是多部分的传输格式(通过注解的方式(@MultipartConfig)告诉servlet)。
try {
profile = req.getPart("profile");
inputStream = profile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (profile == null || inputStream == null) {
throw new RuntimeException("您必须上传头像!");
}
// 需要使用UUID进行处理,使得文件名不可能重复,防止覆盖掉用户头像信息
String fileName = UUID.randomUUID().toString() + "_" + profile.getSubmittedFileName();
// 给文件按照不同日期去创建目录
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String format = LocalDate.now().format(dateTimeFormatter);
String path = Constant.BASEPATH + format;
// 如果路径不存在,就创建路径
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
// 通过自己定义的工具类copy文件
IOUtil.copy(inputStream, path + "/" + fileName);
User user = new User(username, password);
userService.register(user);
}
}
实现的效果:
我们其实可以根据要存入内容的多少,提前预估好要分多少个文件夹去存,然后根据Hash算法算不同路径的哈希值进行取余的操作,最后就存在几个文件夹中。代码实现跟上面按日期基本一致。
@WebServlet("/user/register")
@MultipartConfig
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
// 用于存放文件的部分
Part profile = null;
InputStream inputStream = null;
// 获取文件的部分不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是多部分的传输格式(通过注解的方式(@MultipartConfig)告诉servlet)。
try {
profile = req.getPart("profile");
inputStream = profile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (profile == null || inputStream == null) {
throw new RuntimeException("您必须上传头像!");
}
// 需要使用UUID进行处理,使得文件名不可能重复,防止覆盖掉用户头像信息
String fileName = UUID.randomUUID().toString() + "_" + profile.getSubmittedFileName();
// 需求建立不同文件夹来存储图片
// // ①给文件按照不同日期去创建目录
// DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// String format = LocalDate.now().format(dateTimeFormatter);
// String path = Constant.BASEPATH + format;
// ②按哈希方案去创建文件夹
int pathInt = fileName.hashCode() % 10;
String path = Constant.BASEPATH + pathInt;
// 如果路径不存在,就创建路径
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
// 通过自己定义的工具类copy文件
IOUtil.copy(inputStream, path + "/" + fileName);
User user = new User(username, password);
userService.register(user);
}
}
实现的效果:
也可以实现多层的一个文件夹创建,相当于文件夹套文件夹:
@WebServlet("/user/register")
@MultipartConfig
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
// 用于存放文件的部分
Part profile = null;
InputStream inputStream = null;
// 获取文件的部分不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是多部分的传输格式(通过注解的方式(@MultipartConfig)告诉servlet)。
try {
profile = req.getPart("profile");
inputStream = profile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (profile == null || inputStream == null) {
throw new RuntimeException("您必须上传头像!");
}
// 需要使用UUID进行处理,使得文件名不可能重复,防止覆盖掉用户头像信息
String fileName = UUID.randomUUID().toString() + "_" + profile.getSubmittedFileName();
// 需求建立不同文件夹来存储图片
// // ①给文件按照不同日期去创建目录
// DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// String format = LocalDate.now().format(dateTimeFormatter);
// String path = Constant.BASEPATH + format;
// ②按哈希方案去创建文件夹
int pathInt1 = fileName.hashCode() % 10;
int pathInt2 = profile.getSubmittedFileName().hashCode() % 10;
String path = Constant.BASEPATH + pathInt1 + "/" + pathInt2;
// 如果路径不存在,就创建路径
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
// 通过自己定义的工具类copy文件
IOUtil.copy(inputStream, path + "/" + fileName);
User user = new User(username, password);
userService.register(user);
}
}
可能有人要问了,为什么需要建立那么多的文件夹来分类呢?因为我们可以通过多个文件夹来提升我们的查找效率!
为了美观,我们还是采取不同日期不同文件夹的形式来存储我们的图片。
现在我们基本实现了用户上传头像后将头像保存在我们指定的路径下,那么我们该如何为用户展示他自己的头像呢?
所以我们接下来应该实现将存储头像文件的路径保存在数据库中,让用户对象可以通过数据库读取我们储存的头像。
这样当用户登陆的时候就可以通过用户对象读取数据库中该用户的头像路径,然后通过这个路径给用户展示他自己的头像。
我们可以通过IDEA中tomcat的配置来为我们存储用户图片的文件夹配置一个虚拟路径,操作如下:
通过这个操作,通过tomcat,我们可以使用链接http://localhost:8080/image/1.jpg来访问我们img文件夹下的1.jpg图片文件,我们的项目user-manager也是通过这种配置来访问的(http://localhost:8080/user-manager/…/)。
我们把这个读取文件的浏览器url也放到我们的常量类中,方便取用。
public class Constant {
// 加密用的盐常量
public static final String SALT = "!@#hjjkh!H!JKH";
// 传图片存图片的根路径
public static final String BASEPATH = "E://Java/Projects/learnTomcat/user-manager/www/img/";
// 读取图片的浏览器url
public static final String BASE_URL_PATH = "http://localhost:8080/image/";
}
首先更新我们的User类,给用户添加一个图片路径的属性,再写构建方法
public class User implements Serializable {
private static final Long serialVersionUID = 1L;
private Integer id;
private String username;
private String password;
private String profile;
private Integer deptId;
public User() {
}
public User(String username, String password, String profile) {
this.username = username;
this.password = password;
this.profile = profile;
}
......
更新了User的构建方法,我们继续去更新UserContorller中User的构建传所入的参数。
@WebServlet("/user/register")
@MultipartConfig
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
// 用于存放文件的部分
Part profile = null;
InputStream inputStream = null;
// 获取文件的部分不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是多部分的传输格式(通过注解的方式(@MultipartConfig)告诉servlet)。
try {
profile = req.getPart("profile");
inputStream = profile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (profile == null || inputStream == null) {
throw new RuntimeException("您必须上传头像!");
}
// 需要使用UUID进行处理,使得文件名不可能重复,防止覆盖掉用户头像信息
String fileName = UUID.randomUUID().toString() + "_" + profile.getSubmittedFileName();
// 需求建立不同文件夹来存储图片
// // ①给文件按照不同日期去创建目录
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String format = LocalDate.now().format(dateTimeFormatter);
String path = Constant.BASEPATH + format;
// ②按哈希方案去创建文件夹
// int pathInt1 = fileName.hashCode() % 10;
// int pathInt2 = profile.getSubmittedFileName().hashCode() % 10;
// String path = Constant.BASEPATH + pathInt1 + "/" + pathInt2;
// 如果路径不存在,就创建路径
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
// 通过自己定义的工具类copy文件
IOUtil.copy(inputStream, path + "/" + fileName);
User user = new User(username, password, Constant.BASE_URL_PATH + format + "/" + fileName);
userService.register(user);
}
}
那同理,为了能给数据库存入profile的路径,跟数据库打交道的UserDao里的代码也要做对应的更改:
public class UserDao extends BaseDao {
// 保存用户
public int save(User user) {
String sql = "insert into user(username,password,profile) values (?,?,?)";
PreparedStatement statement = null;
Connection conn = getConn();
try {
statement = conn.prepareStatement(sql);
statement.setString(1, user.getUsername());
statement.setString(2, user.getPassword());
statement.setString(3, user.getProfile());
// 受影响的行数
return statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, null);
}
return 0;
}
}
现在我们实现了存放虚拟路径到数据库的操作,也就实现了从user对象中获取这个虚拟路径的功能!
代码优化的过程就是在做减法,可能不要再一个类里实现多个功能,使整体代码的耦合性更低。我们来看我们目前的UserContorller类代码:
@WebServlet("/user/register")
@MultipartConfig
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
// 用于存放文件的部分
Part profile = null;
InputStream inputStream = null;
// 获取文件的部分不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是多部分的传输格式(通过注解的方式(@MultipartConfig)告诉servlet)。
try {
profile = req.getPart("profile");
inputStream = profile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (profile == null || inputStream == null) {
throw new RuntimeException("您必须上传头像!");
}
// 需要使用UUID进行处理,使得文件名不可能重复,防止覆盖掉用户头像信息
String fileName = UUID.randomUUID().toString() + "_" + profile.getSubmittedFileName();
// 需求建立不同文件夹来存储图片
// // ①给文件按照不同日期去创建目录
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String format = LocalDate.now().format(dateTimeFormatter);
String path = Constant.BASEPATH + format;
// ②按哈希方案去创建文件夹
// int pathInt1 = fileName.hashCode() % 10;
// int pathInt2 = profile.getSubmittedFileName().hashCode() % 10;
// String path = Constant.BASEPATH + pathInt1 + "/" + pathInt2;
// 如果路径不存在,就创建路径
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
// 通过自己定义的工具类copy文件
IOUtil.copy(inputStream, path + "/" + fileName);
User user = new User(username, password, Constant.BASE_URL_PATH + format + "/" + fileName);
userService.register(user);
}
}
我们可以把获取日期文件夹名字这个功能独立出去交给一个工具类来完成——DateUtil:
public class DateUtil {
public static String getNowString(String pattern) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern);
return LocalDate.now().format(dateTimeFormatter);
}
public static String getNowString() {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
return LocalDate.now().format(dateTimeFormatter);
}
}
更新我们的UserContorller类代码:
@WebServlet("/user/register")
@MultipartConfig
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
// 用于存放文件的部分
Part profile = null;
InputStream inputStream = null;
// 获取文件的部分不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是多部分的传输格式(通过注解的方式(@MultipartConfig)告诉servlet)。
try {
profile = req.getPart("profile");
inputStream = profile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (profile == null || inputStream == null) {
throw new RuntimeException("您必须上传头像!");
}
// 需要使用UUID进行处理,使得文件名不可能重复,防止覆盖掉用户头像信息
String fileName = UUID.randomUUID().toString() + "_" + profile.getSubmittedFileName();
// 需求建立不同文件夹来存储图片
// ①给文件按照不同日期去创建目录
String format = DateUtil.getNowString();
String path = Constant.BASEPATH + format;
// ②按哈希方案去创建文件夹
// int pathInt1 = fileName.hashCode() % 10;
// int pathInt2 = profile.getSubmittedFileName().hashCode() % 10;
// String path = Constant.BASEPATH + pathInt1 + "/" + pathInt2;
// 如果路径不存在,就创建路径
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
// 通过自己定义的工具类copy文件
IOUtil.copy(inputStream, path + "/" + fileName);
User user = new User(username, password, Constant.BASE_URL_PATH + format + "/" + fileName);
userService.register(user);
}
}
这样我们就进一步降低我们代码之间耦合性了。
我们需要实现一个功能,就是当鼠标离开用户名的输入框时显示这个用户名能不能被注册,这是怎么实现的。
要实现这个功能必须保证两点:
1、页面不能刷新,页面一旦刷新,所有的内容都会重置;
2、blur事件一发生,主动去数据库查询有没有这个用户。
如果以上的效果不需要查询数据库其实很好实现,添加blur事件,修改dom即可。我们要学习的其实是怎么在事件的回掉函数中发送http请求而已,其实http请求只是个报文而已,java、js、postman,浏览器都是可以发送的。而在js中我们用的就是ajax这项技术。
想一想有哪些功能我们无法实现:
思考:为什么做不到这些呢?
在此之前,我们可以通过以下几种方式让浏览器发出对服务端的请求,获得服务端的数据:
这些方案都是我们无法通过或者很难通过代码的方式进行编程(对服务端发出请求并且接受服务端返回的响应),如果我们可以通过 JavaScript 直接发送网络请求,动态的去更新页面,那么 Web 的可能就会更多,随之能够实现的功能也会更多。
AJAX (Asynchronous Javascript And XML)就是浏览器提供的一套 API,可以通过 JavaScript 调用,从而实现通过代码控制请求与响应。实现通过 JavaScript 进行网络编程。
至于 XML:最早在客户端与服务端之间传递数据时所采用的数据格式就是 XML,现在已经不是了,我们用java。
AJAX API 中核心提供的是一个 XMLHttpRequest
类型,所有的 AJAX 操作都需要使用到这个类型。
使用 AJAX 的过程可以类比平常我们访问网页过程
// 1. 创建一个 XMLHttpRequest 类型的对象 —— 相当于打开了一个浏览器
var xhr = new XMLHttpRequest()
// 2. 打开与一个网址之间的连接 —— 相当于在地址栏输入访问地址
xhr.open('GET', '/time')
// 3. 通过连接发送一次请求 —— 相当于回车或者点击访问发送请求
xhr.send(null)
// 4. 指定 xhr 状态变化事件处理函数 —— 相当于处理网页呈现后的操作
xhr.onreadystatechange = function () {
// 通过 xhr 的 readyState 判断此次请求的响应是否接收完成
if (this.readyState === 4) {
// 通过 xhr 的 responseText 获取到响应的响应体
console.log(this.responseText)
}
}
注意:涉及到 AJAX 操作的页面不能使用文件协议访问(文件的方式访问)
由于 readystatechange事件(readyState)是在 xhr
对象状态变化时触发(不单是在得到响应时),也就意味着这个事件会被触发多次,所以我们有必要了解每一个状态值代表的含义:
readyState | 状态描述 | 说明 |
---|---|---|
0 | UNSENT | 代理(XHR)被创建,但尚未调用 open() 方法。 |
1 | OPENED | open() 方法已经被调用,建立了连接。 |
2 | HEADERS_RECEIVED | send() 方法已经被调用,并且已经可以获取状态行和响应头。 |
3 | LOADING | 响应体下载中, responseText 属性可能已经包含部分数据。 |
4 | DONE | 响应体下载完成,可以直接使用 responseText 。 |
时间轴
var xhr = new XMLHttpRequest()
console.log(xhr.readyState)
// => 0
// 初始化 请求代理对象
xhr.open('GET', '/time')
console.log(xhr.readyState)
// => 1
// open 方法已经调用,建立一个与服务端特定端口的连接
xhr.send()
xhr.addEventListener('readystatechange', function () {
switch (this.readyState) {
case 2:
// => 2
// 已经接受到了响应报文的响应头
// 可以拿到头
// console.log(this.getAllResponseHeaders())
console.log(this.getResponseHeader('server'))
// 但是还没有拿到体
console.log(this.responseText)
break
case 3:
// => 3
// 正在下载响应报文的响应体,有可能响应体为空,也有可能不完整
// 在这里处理响应体不保险(不可靠)
console.log(this.responseText)
break
case 4:
// => 4
// 一切 OK (整个响应报文已经完整下载下来了)
// 这里处理响应体
console.log(this.responseText)
break
}
})
通过理解每一个状态值的含义得出一个结论:一般我们都是在 readyState
值为 4
时,执行响应的后续逻辑。
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
// 后续逻辑......
}
}
GET 请求
通常在一次 GET 请求过程中,参数传递都是通过 URL 地址中的
?
参数传递。
var xhr = new XMLHttpRequest()
// GET 请求传递参数通常使用的是问号传参
// 这里可以在请求地址后面加上参数,从而传递数据到服务端
xhr.open('GET', '/delete?id=1')
// 一般在 GET 请求时无需设置响应体,可以传 null 或者干脆不传
xhr.send(null)
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
console.log(this.responseText)
}
}
// 一般情况下 URL 传递的都是参数性质的数据,而 POST 一般都是业务数据
POST 请求过程中,都是采用请求体承载需要提交的数据。
var xhr = new XMLHttpRequest()
// open 方法的第一个参数的作用就是设置请求的 method
xhr.open('POST', '/add')
// 设置请求头中的 Content-Type 为 application/x-www-form-urlencoded
// 标识此次请求的请求体格式为 urlencoded 以便于服务端接收数 据
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
// 需要提交到服务端的数据可以通过 send 方法的参数传递
// 格式:name=zhangsan&age=18
xhr.send('name=zhangsan&age=18')
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
console.log(this.responseText)
}
}
关于同步与异步的概念在生活中有很多常见的场景,举例说明。
- 同步:一个人在同一个时刻只能做一件事情,在执行一些耗时的操作(不需要看管)不去做别的事,只是等待
- 异步:在执行一些耗时的操作(不需要看管)去做别的事,而不是等待
xhr.open()
方法第三个参数要求传入的是一个 bool
值,其作用就是设置此次请求是否采用异步方式执行,默认为 true
,如果需要同步执行可以通过传递 false
实现:
console.log('before ajax')
var xhr = new XMLHttpRequest()
// 默认第三个参数为 true 意味着采用异步方式执行
xhr.open('GET', '/time', true)
xhr.send(null)
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
// 这里的代码最后执行
console.log('request done')
}
}
console.log('after ajax')
如果采用同步方式执行,则代码会卡死在 xhr.send()
这一步:
console.log('before ajax')
var xhr = new XMLHttpRequest()
// 同步方式
xhr.open('GET', '/time', false)
// // 同步方式 执行需要 先注册事件再调用 send,否则 readystatechange 无法触发
// xhr.onreadystatechange = function () {
// if (this.readyState === 4) {
// // 这里的代码最后执行
// console.log('request done')
// }
// }
xhr.send(null)
// 因为 send 方法执行完成 响应已经下载完成
console.log(xhr.responseText)
console.log('after ajax')
演示同步异步差异。
了解同步模式即可,切记不要使用同步模式。
至此,我们已经大致了解了 AJAX 所提供的基本 API 。
function ajax(method, url, data, fun) {
var xhr = new XMLHttpRequest()
xhr.open(method, url)
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
xhr.send(data)
xhr.addEventListener('readystatechange', function () {
if (this.readyState === 4) {
// 回调函数传入详情内容
fun(this.responseText);
}
})
}
其实我们不难发现,ajax也就是帮助我们实现了一个在不刷新页面的情况下给服务器发送请求后并拿到服务器返回响应进行逻辑处理的功能(监听鼠标离开表单等等操作)。对我们其余任何业务逻辑并没有帮助。我们只用记住当我们需要实现不刷新浏览器页面的情况下使浏览器给服务器发送请求(异步),我们就用ajax就行了!
我们根据上面的ajax简介改一份属于我们自己的代码出来:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
Title
用户管理系统
我们封装了一个function来实现ajax的功能,其中传入一个回调函数fun来处理我们收到响应(用户名是否可用)后的业务逻辑。
现在我们实现了监听用户名输入栏的操作,就是当监听到我们的鼠标离开用户名输入框后,就会往/user/cheakUserName
发送一个POST请求。当然这个POST请求的具体参数就是我们输入框中输入的username。
目前收到请求后针对请求回应响应的业务逻辑还没写(cheakUserName的Servlet,与数据库中用户名匹配)。
等收到服务器的响应后,在回调函数fun里完成相关业务逻辑也还没写(显示用户名已存在还是可以使用)。
上面的代码很多都是没有用的注释,我们了解完原理之后,去掉没有用的语句,其实自己封装的ajax函数很简单,如下所示:
现在我们实现了当监听到鼠标离开用户名输入框后,就会往/user/cheakUserName
发送POST请求。我们接下来需要写一个cheakUserName的Servlet来处理/user/cheakUserName
的业务逻辑,具体是与数据库的用户名去进行匹配,看有没有重复的用户名(可不可用),之后将这个结果封装成响应发送给浏览器。
@WebServlet("/user/checkUserName")
public class CheckUserNameController extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
Boolean flag = userService.CheckUserName(username);
// 根据CheckUserName的返回值不同执行不同的响应封装
PrintWriter writer = resp.getWriter();
if (flag) {
// 有重复的名字返回yes
writer.write("yes");
} else {
writer.write("no");
}
}
}
我们写完了具体的业务逻辑判断,实现了根据数据库是否有重复名字的封装不同的响应给浏览器。至于具体的判断逻辑,还得交给我们的M层UserService去做。
接下来我们写UserService的业务逻辑:
public class UserService {
UserDao userDao = new UserDao();
// 执行业务逻辑
public void register(User user) {
// 设计密码(加盐)
String password = user.getUsername() + user.getPassword() + Constant.SALT;
// 将密码进行MD5加密运算后再存入数据库(加密)
user.setPassword(MD5Util.digest(password));
// 实质是向数据库插入一条数据
userDao.save(user);
}
public Boolean CheckUserName(String username) {
// 掉用UserDao的findUserByName方法去返回相同username的List表
// 如果表的容量大于0,说明有重复的名字,返回true
List<User> users = userDao.findUserByName(username);
return users.size() > 0;
}
}
这样我们就写完了UserService的具体CheckUserName业务逻辑,根据UserDao返回的List表的尺寸去判断有没有重复的用户,然后给C层的CheckUserNameController返回一个Boolean类型的结果。
所有跟数据库相关的操作还得靠我们的UserDao来完成,接下来我们更新UserDao的相关代码:
public class UserDao extends BaseDao {
// 保存用户
public int save(User user) {
String sql = "insert into user(username,password,profile) values (?,?,?)";
PreparedStatement statement = null;
Connection conn = getConn();
try {
statement = conn.prepareStatement(sql);
statement.setString(1, user.getUsername());
statement.setString(2, user.getPassword());
statement.setString(3, user.getProfile());
// 受影响的行数
return statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, null);
}
return 0;
}
// 根据username查找相同名字的用户集合
public List<User> findUserByName(String username) {
String sql = "select id,username,password,profile,dept_id from user where username = ?";
PreparedStatement statement = null;
Connection conn = getConn();
ResultSet resultSet = null;
// 存放取出的user对象
List<User> users = new ArrayList<>();
try {
statement = conn.prepareStatement(sql);
statement.setString(1, username);
resultSet = statement.executeQuery();
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username1 = resultSet.getString("username");
String password = resultSet.getString("password");
String profile = resultSet.getString("profile");
int deptId = resultSet.getInt("dept_id");
User user = new User(username1, password, profile);
user.setId(id);
user.setDeptId(deptId);
users.add(user);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, resultSet);
}
return users;
}
}
我们通过UserDao实现了findUserByName功能,通过传入用户名username来查找数据库中所有相同名字的用户,并给满足条件的每一个用户创建一个用户对象存入List集合中返回。
现在已经实现了通过监听鼠标离开输入栏的操作,发送POST请求,并且获得了返回来的判断是否有用户名重复的结果的响应。我们现在就剩最后一步操作了,根据这个响应在页面上显示对应的信息。
注:上面这个整体的代码逻辑非常值得学习!!一个很复杂的需求层层分配下来,每个部分只用完成属于他的那一小撮工作,然后把工作就交给下一个类去处理!这样思维写出来的代码耦合性很低,而且所有功能基本都是分别封装的,复用也非常方便!
我们现在实现了检测到鼠标离开用户名输入框后,将我们输入的用户名通过POST请求发送给服务器做相应业务逻辑处理(判断数据库中有没有这个名字),然后服务器会根据业务逻辑处理的结果给浏览器发送一个响应回来(用户名可不可用)。
现在这个回调函数fun,要实现的功能就是根据这个响应的不同来实现页面上显示不同的话(用户名可用/不可用)。
我们修改register.jsp的代码,给表单用户名的下面加一个元素来显示我们想显示的提示。这样我们就可以在回调函数中使用
document.getElementById("msg").innerText = "用户名已经存在!"
来给我们的这个元素添加文字提示。修改后代码如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
用户管理系统
用户管理系统
通过上面的操作,我们就已经实现了我们所需要的功能,根据用户所输入的用户名来提醒用户名可不可用。
我们上面自己封装了一个ajax来实现了异步操作,其实有很多好用的api已经帮我们封装好了,我们改用人家的api试一试怎么使用。
在bootcdn网站里搜索想要的api就可以了
点进去找到对应需要的cdn进行复制,在jsp中粘贴过去就行了。
当然也可以在对应的官网中找到cdn引用。
可以去用浏览器打开cdn的链接,然后去下载这个js文件,加入到我们项目的静态资源里。
就可以直接用本地路径去引用了!
我们引用jquery的ajax-api来实现了我们所需要的功能,不用自己封装ajax了!更改jsp代码:(不知道怎么写不要紧,重要的是要会查文档!)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
用户管理系统
用户管理系统
当然也可以调用这个api更方便的方法,post方法,也可以实现我们需要的功能,发送Post请求并根据响应执行回调函数。
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js"></script>
<script>
// id为username的输入框中的输入值
let usernameInput = document.getElementById("username");
// 通过ajax增加监听,当鼠标离开输入框后,使浏览器向服务器发送一个POST请求,来传递输入框输入的内容
usernameInput.addEventListener("blur", function () {
$.post("user/checkUserName",{username: usernameInput.value},
function(data) {
if (data == "yes") {
document.getElementById("msg").style.color = "red"
document.getElementById("msg").innerText = "用户名已经存在!"
} else {
document.getElementById("msg").style.color = "black"
document.getElementById("msg").innerText = "恭喜您!用户名可以使用"
}
});
})
</script>
除了jquery外,还有axios的ajax-api可以被我们调用,这是目前最主流的实现ajax的api。我们使用这个api来实现我们的功能,老规矩,不知道怎么用就查文档,文档里写的清清楚楚的!
这样我们就根据ajax-api更改了我们的请求:
但是这样运行结果是不对的,因为我们发现他不能正确返回数据库有无用户的信息。原因找到如下:
因为它是最主流的api,所以他默认发送的Content-Type是目前主流使用的application/json格式,与我们需要的application/x-www-form-urlencoded不符。所以我们通过查文档来修改错误的地方:
<script src="static/js/axios.min.js"></script>
<script>
// 通过查文档,创建了一个新实例解决了Content-Type的问题
const instance = axios.create({
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
});
// id为username的输入框中的输入值
let usernameInput = document.getElementById("username");
// 通过ajax增加监听,当鼠标离开输入框后,使浏览器向服务器发送一个POST请求,来传递输入框输入的内容
usernameInput.addEventListener("blur", function () {
instance.post("user/checkUserName", "username=" + usernameInput.value)
.then(function (response) {
if (response.data == "yes") {
document.getElementById("msg").style.color = "red"
document.getElementById("msg").innerText = "用户名已经存在!"
} else {
document.getElementById("msg").style.color = "black"
document.getElementById("msg").innerText = "恭喜您!用户名可以使用"
}
})
.catch(function (error) {
console.log(error);
});
})
</script>
这样就完成了使用axios中的ajax-api实现了我们的需求。
目前注册的核心功能已经写完了,还剩一些地方没有完善,我们来把这个功能完善一下。
首先,我们UserService类中注册方法 的 调用UserDao向数据库插入数据 的指令,这个应该要加一个逻辑判断,不要让相同用户名的注册请求再发一次。更新代码如下:
public class UserService {
UserDao userDao = new UserDao();
// 执行业务逻辑
public void register(User user) {
// 设计密码(加盐)
String password = user.getUsername() + user.getPassword() + Constant.SALT;
// 将密码进行MD5加密运算后再存入数据库(加密)
user.setPassword(MD5Util.digest(password));
// 处理用户名相同的情况
List<User> userByName = userDao.findUserByName(user.getUsername());
if (userByName.size() > 0) {
throw new UserIsExistException();
} else {
// 实质是向数据库插入一条数据
userDao.save(user);
}
}
public Boolean checkUserName(String username) {
// 掉用UserDao的findUserByName方法去返回相同username的List表
// 如果表的容量大于0,说明有重复的名字,返回true
List<User> users = userDao.findUserByName(username);
return users.size() > 0;
}
}
我们选择在发现有重复用户的时候抛出一个自定义的异常,以供我们后续工作中捕获做处理,我们首先要来完成这个异常。
我们创建一个exception异常文件夹,用于存放我们自定义的异常类,我们先定义一个用户已经存在的异常:
/**
* 用户已经存在的异常
*/
public class UserIsExistException extends RuntimeException {
public UserIsExistException() {
super("用户已经存在");
}
}
接下来我们需要在后面运行阶段捕获这个异常来做对应的处理。肯定是调用这个注册方法的时候做异常的处理呀!我们修改UserController类中的代码,去捕获这个异常!
@WebServlet("/user/register")
@MultipartConfig
public class UserContorller extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
// 1. 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
// 用于存放文件的部分
Part profile = null;
InputStream inputStream = null;
// 获取文件的部分不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是多部分的传输格式(通过注解的方式(@MultipartConfig)告诉servlet)。
try {
profile = req.getPart("profile");
inputStream = profile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (profile == null || inputStream == null) {
throw new RuntimeException("您必须上传头像!");
}
// 需要使用UUID进行处理,使得文件名不可能重复,防止覆盖掉用户头像信息
String fileName = UUID.randomUUID().toString() + "_" + profile.getSubmittedFileName();
// 需求建立不同文件夹来存储图片
// ①给文件按照不同日期去创建目录
String format = DateUtil.getNowString();
String path = Constant.BASEPATH + format;
// ②按哈希方案去创建文件夹
// int pathInt1 = fileName.hashCode() % 10;
// int pathInt2 = profile.getSubmittedFileName().hashCode() % 10;
// String path = Constant.BASEPATH + pathInt1 + "/" + pathInt2;
// 如果路径不存在,就创建路径
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
// 通过自己定义的工具类copy文件
IOUtil.copy(inputStream, path + "/" + fileName);
User user = new User(username, password, Constant.BASE_URL_PATH + format + "/" + fileName);
// 去捕获用户存在的异常
try {
userService.register(user);
resp.sendRedirect(req.getContextPath() + "/pages/seccess.jsp");
} catch (UserIsExistException | IOException e) {
try {
resp.sendRedirect(req.getContextPath() + "/pages/error.jsp");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
我们增加的代码逻辑就是重定向,如果捕获到异常就证明用户名重复,我们把他定向到一个失败的页面,反之定向到成功的页面。失败成功页面自己随便写一下就行,放到根目录下的pages文件夹(表明可以直接公开访问)里。
用户名和密码和头像为空等等的判断,后续自己练习。
我们首先要写一个登录页面,这个页面我们想实现功能:鼠标输完用户名后在用户名上面显示该用户对应的头像。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
用户管理系统-登陆
用户管理系统-登陆
在这个页面里,我们相当于还差一个servlet来处理user/getProfile
这个路径的业务逻辑,接下来我们来完成这个Servlet。
我们继续写一个GetProfileController来完成我们的对应url下的业务处理:
@WebServlet("/user/getProfile")
public class GetProfileController extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
// 通过用户名查询数据库获取头像路径
String profile = userService.getProfile(username);
if (profile != null) {
resp.getWriter().write(profile);
}
}
}
我们完成了业务处理,具体和用户相关的操作还得交给UserService来处理,接下来我们更新它的代码:
public class UserService {
UserDao userDao = new UserDao();
// 执行业务逻辑
public void register(User user) {
// 设计密码(加盐)
String password = user.getUsername() + user.getPassword() + Constant.SALT;
// 将密码进行MD5加密运算后再存入数据库(加密)
user.setPassword(MD5Util.digest(password));
// 处理用户名相同的情况
List<User> userByName = userDao.findUserByName(user.getUsername());
if (userByName.size() > 0) {
throw new UserIsExistException();
} else {
// 实质是向数据库插入一条数据
userDao.save(user);
}
}
public Boolean checkUserName(String username) {
// 掉用UserDao的findUserByName方法去返回相同username的List表
// 如果表的容量大于0,说明有重复的名字,返回true
List<User> users = userDao.findUserByName(username);
return users.size() > 0;
}
// 根据用户名获取头像路径
public String getProfile(String username) {
List<User> users = userDao.findUserByName(username);
if (users.size() > 0) {
return users.get(0).getProfile();
}
return null;
}
}
这里其实就已经完成了我们的业务逻辑,没有更新UserDao的方法,这是因为之前我们在UserDao中写的findUserByName方法在这里被我们复用了!这也就是我们为什么要分这么多类的原因。
现在该实现登陆的业务逻辑了,写user/login
的servlet:
@WebServlet("/user/login")
public class LoginController extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
User user = new User();
user.setUsername(username);
user.setPassword(password);
resp.setHeader("Content-Type", "text/plain;charset=utf-8");
try {
userService.login(user);
// 保存登陆状态
HttpSession session = req.getSession();
session.setAttribute("user", user);
// 页面跳转
resp.sendRedirect(req.getContextPath() + "/pages/success.jsp");
} catch (UserIsNotExistException e) {
resp.getWriter().write("用户不存在");
} catch (PasswordIncorrectException e) {
resp.getWriter().write("密码错误");
}
}
}
接下来写UserService的login功能
public class UserService {
UserDao userDao = new UserDao();
// 执行业务逻辑
public void register(User user) {
// 设计密码(加盐)
String password = user.getUsername() + user.getPassword() + Constant.SALT;
// 将密码进行MD5加密运算后再存入数据库(加密)
user.setPassword(MD5Util.digest(password));
// 处理用户名相同的情况
List<User> userByName = userDao.findUserByName(user.getUsername());
if (userByName.size() > 0) {
throw new UserIsExistException();
} else {
// 实质是向数据库插入一条数据
userDao.save(user);
}
}
public Boolean checkUserName(String username) {
// 掉用UserDao的findUserByName方法去返回相同username的List表
// 如果表的容量大于0,说明有重复的名字,返回true
List<User> users = userDao.findUserByName(username);
return users.size() > 0;
}
// 根据用户名获取头像路径
public String getProfile(String username) {
List<User> users = userDao.findUserByName(username);
if (users.size() > 0) {
return users.get(0).getProfile();
}
return null;
}
public void login(User user) {
List<User> users = userDao.findUserByName(user.getUsername());
// 如果找不到,肯定是用户名不存在,抛用户不存在异常
if (users.size() == 0) {
throw new UserIsNotExistException();
}
// 拿到对应用户名的用户
User realUser = users.get(0);
// 要对用户新传入的密码进行加盐加密的处理,才能进行比较
if (user.getUsername() != null && user.getPassword() != null) {
String password = user.getUsername() + user.getPassword() + Constant.SALT;
password = MD5Util.digest(password);
if (!password.equals(realUser.getPassword())) {
throw new PasswordIncorrectException();
}
}
}
}
写出这两个自定义异常
public class PasswordIncorrectException extends RuntimeException {
public PasswordIncorrectException() {
super("密码错误的异常!");
}
}
public class UserIsNotExistException extends RuntimeException {
public UserIsNotExistException() {
super("用户不存在!");
}
}
我们完成了登陆功能的实现。
我们的登陆请求只要检测到user/login
的url就会被执行对应的servlet业务逻辑,但是如果有人想破解某个用户的密码,直接去用Java写一个for循环或者PostMan工具去暴力循环所有密码的可能性,一直发请求,我们的服务器可能会崩溃,或者用户密码被泄露。
为了避免这种情况,我们需要加入验证码功能,只有完成了验证码的操作,请求才会被服务器处理,才会去执行Servlet业务逻辑。
验证码写起来非常繁琐麻烦,对我们帮助不大,随便cv一个过来看看就行了,知道实现原理就行。
我们要cv一个Servlet实现我们验证码生成的功能。这个servlet要能生成随机字符串,然后把这个随机字符串画成机器难以辨别的图显示出来,还要把这个随机字符串存进Session中,方便等我们收到用户的验证码答案后进行比对。
/user/verification
的servlet业务逻辑如下所示:
@WebServlet("/user/verification")
public class IdentityServlet extends HttpServlet {
private static final char[] chars={'0','1','2','3','4','5','6','7','8','9','A','B'};//自定义验证码池
private final static Random random = new Random();
//获取6位随机数,放在图片里
private static String getRandomString(){
StringBuilder buffer = new StringBuilder();
for(int i = 0; i < 6; i++){
buffer.append(chars[random.nextInt(chars.length)]);
}
return buffer.toString();
}
//获取随机的颜色
private static Color getRandomColor(){
return new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255));
}
//返回某颜色的反色
private static Color getReverseColor(Color c){
return new Color(255 - c.getRed(), 255 - c.getGreen(), 255 - c.getBlue());
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//设置输出类型
response.setContentType("image/jpeg");
//随机字符串
String verification = getRandomString();
request.getSession(true).setAttribute("verification", verification);//放到session里
//图片宽度
int width = 100;
//图片高度
int height = 30;
//随机颜色,用于背景色
Color color = getRandomColor();
//反色,用于前景色
Color reverse = getReverseColor(color);
//创建一个彩色图片
BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//绘图对象
Graphics2D g = bi.createGraphics();
//设置字体
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD,16));
//设置颜色
g.setColor(color);
//绘制背景
g.fillRect(0, 0, width, height);
g.setColor(reverse);
//绘制随机字符
g.drawString(verification, 18, 20);
//画100个噪音点
for(int i = 0; i < 50;i++){
g.drawRect(random.nextInt(width), random.nextInt(height), 1, 1);
}
//转成JPEG格式
ServletOutputStream out= response.getOutputStream();
//对图片进行编码输出
ImageIO.write(bi, "jpeg", out);
out.flush();
}
}
在前端我们应该在login.jsp页面上显示出验证码。只能在我们的表单中添加一行来访问/user/verification
这个我们刚写的业务逻辑,拿到生成的图片显示出来就行。在表单中密码那一行下面增加这么一行验证码的显示就行。
<div class="form-group">
<label for="password">验证码:label>
<input type="password" class="form-control" id="verify" name="verify" placeholder="验证码">
<img src="user/verification">
div>
现在我们的前端页面会有填写验证码的那一栏,当我们输入完毕后,验证码会随着用户名密码一起作为POST请求被发送到服务器中,下来我们应该修改登陆的Servlet来增加验证码判别这一操作。
在LoginController这个Servlet中还要加上验证码的判断业务逻辑:
@WebServlet("/user/login")
public class LoginController extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("Content-Type", "text/plain;charset=utf-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
// 拿到用户输入的验证码
String verify = req.getParameter("verify");
// 从session中拿到原本的验证码答案
HttpSession session = req.getSession();
String verification = (String)session.getAttribute("verification");
// 比较用户输入的验证码对不对
if (!verification.equals(verify)) {
resp.getWriter().write("验证码错误!");
return;
}
// 验证码正确才会执行下面验证用户名密码的逻辑
User user = new User();
user.setUsername(username);
user.setPassword(password);
try {
userService.login(user);
// 保存登陆状态
session = req.getSession();
session.setAttribute("user", user);
// 页面跳转
resp.sendRedirect(req.getContextPath() + "/pages/success.jsp");
} catch (UserIsNotExistException e) {
resp.getWriter().write("用户不存在");
} catch (PasswordIncorrectException e) {
resp.getWriter().write("密码错误");
}
}
}
现在如果验证码输入不对,则不会执行后面的数据库比对等业务逻辑,完成了验证码功能的实现。
其实还是用到的跟显示用户名和图片一样的功能实现,监听器。
我们需要给图片合格元素设置一个监听器,当监听到它被点击的时候,就把他自身的src属性(“user/verification”)换成一个新的"user/verification",就实现了刷新的功能。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
用户管理系统-登陆
用户管理系统-登陆
我们先对Servlet的名字进行一个规范化处理:
更改一下名字:
把命名规范了,才好理解项目里哪一部分是实现什么功能的。
写一个servlet来处理"/user"的请求,实现展示所有用户的业务逻辑
@WebServlet("/user")
public class UserServlet extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 查询所有的用户
List<User> users = userService.findAllUsers();
// 转发给user.jsp
req.setAttribute("users", users);
req.getRequestDispatcher("/WEB-INF/pages/user/user.jsp").forward(req, resp);
}
}
这里面要用到与用户相关的处理逻辑,扔给UserService来处理,在里面新增一个findAllUsers()
方法:
// 调用UserDao层的方法找所有的用户
// 一定要注意MVC的思想,虽然只有一行代码,但是也必须写在UserService中,不能直接去调用Dao来完成操作!!必须由UserService再去找Dao操作才合理!
public List<User> findAllUsers() {
return userDao.findAllUsers();
}
接下来业务又要扔到Dao层去处理,给UserDao新增findAllUsers()
方法:
// 获取所有的用户
public List<User> findAllUsers() {
String sql = "select id,username,password,profile,dept_id from user";
PreparedStatement statement = null;
Connection conn = getConn();
ResultSet resultSet = null;
// 存放取出的user对象
List<User> users = new ArrayList<>();
try {
statement = conn.prepareStatement(sql);
resultSet = statement.executeQuery();
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String profile = resultSet.getString("profile");
int deptId = resultSet.getInt("dept_id");
User user = new User(username, password, profile);
user.setId(id);
user.setDeptId(deptId);
users.add(user);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, resultSet);
}
return users;
}
这样,我们就获取到了用户的所有信息,接下来该写一个jsp来对用户信息做一个展出。
去找BootStrap里随便找一个css表格样式,cv一下来当做展示用户的表格
当然别忘了在jsp的中加上CDN支持:
用户展示页面
我们完善所有相关代码,新写的user.jsp用户展示界面如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--导入JSTL标签库--%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
已用户展示页面
注册用户信息展示
id
用户名
头像
部门
<%--使用JSTL标签库从请求域中循环遍历拿到每一个用户--%>
${user.id}
${user.username}
${user.deptId}
我们想实现在用户信息展示界面新增一个按钮,通过点击按钮来实现对应数据库的删除操作。
首先再去BootStrap找一个按钮放在表格的后面,我们就通过点击它来执行删除操作。
加入到表格的后面:
id
用户名
头像
部门
操作
<%--使用JSTL标签库从请求域中循环遍历拿到每一个用户--%>
${user.id}
${user.username}
${user.deptId}
这样就实现了我们按钮的添加:
然后我们要思考一下删除的逻辑是什么?
我们点击删除按钮后,怎么获取到删除按钮这行的用户id呢?
我们通过给按钮设置一个属性值来存放用户id的方式来解决。
${user.id}
${user.username}
${user.deptId}
这样我们就可以给按钮添加一个click
事件,来获取到当前点击按钮的用户id了!
我们还是跟用户名可用性一样,当点击操作发生后,采取ajax来发送一个请求给服务器并接受到服务器响应进行处理展示。
这里我们使用jquery的ajax-api来写我们的ajax操作。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--导入JSTL标签库--%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
用户展示页面
已注册用户信息展示
id
用户名
头像
部门
操作
<%--使用JSTL标签库从请求域中循环遍历拿到每一个用户--%>
${user.id}
${user.username}
${user.deptId}
这样我们就实现了我们的前端部分工作,我们点击按钮后就会发送一个get请求给服务器,让他来删除我们对应id的用户;等到删除成功后收到成功的响应了,就会刷新当前页面,这样就完成了功能实现。
我们应该先写Servlet业务逻辑处理/user/delete
的删除请求
@WebServlet("/user/delete")
public class UserDeleteServlet extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 拿到id
String id = req.getParameter("id");
// 删除元素
userService.deleteById(id);
// 给一个删除成功的响应
resp.getWriter().write("ok");
}
}
跟之前一模一样,遇到该UserService和UserDao处理的工作就往后丢就完事了,我们把这两个处理的功能逻辑也完成一下。
UserService里加一个删除方法:
public void deleteById(String id) {
// 调用UserDao层删除用户,虽然只有一行代码,但是也必须写在UserService中!
userDao.deleteById(id);
}
UserDao里也要加入一个删除方法:
// 通过id删除用户的功能,返回数据库受影响的行数
public int deleteById(String id) {
String sql = "delete from user where id = ?";
PreparedStatement statement = null;
Connection conn = getConn();
try {
statement = conn.prepareStatement(sql);
statement.setString(1, id);
// 受影响的行数
return statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, null);
}
return 0;
}
这种是前段的工作,我们不用细纠样式,把功能做出来就行了。
其实实现很简单,在发送删除请求前新增一个判断,如果点击确定在执行删除请求的发送。
更新user.jsp代码如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--导入JSTL标签库--%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
用户展示页面
已注册用户信息展示
id
用户名
头像
部门
操作
<%--使用JSTL标签库从请求域中循环遍历拿到每一个用户--%>
${user.id}
${user.username}
${user.deptId}
首先增加我们的修改按钮:
<%--使用JSTL标签库从请求域中循环遍历拿到每一个用户--%>
${user.id}
${user.username}
${user.deptId}
修改
这样点击修改按钮就会使浏览器访问user/update?id=${user.id}
链接,我们接下来只需要根据这个url写我们对应的业务逻辑就行了。
我们需要一个servlet来实现根据user/update
发送的携带用户id的Get请求,来处理对应的业务逻辑。
主要功能是需要根据id来获取该用户的全部信息,然后把这个用户存进请求域中转发给一个jsp来做显示功能,实现用户信息的回显。这样我们才更符合我们修改用户信息的逻辑。
首先写Servlet的业务逻辑:
@WebServlet("/user/update")
public class UserUpdateServlet extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 从参数中获取id
String id = req.getParameter("id");
// 根据id查询用户
User user = userService.findUserById(Integer.parseInt(id));
// 转发给user.jsp
req.setAttribute("user", user);
req.getRequestDispatcher("/WEB-INF/pages/user/update.jsp").forward(req, resp);
}
}
我们发现又要跟UserService和UserDao打交道了,还是原来那个老一套。
先在UserService里新增findUserById
方法:
public User findUserById(Integer id) {
// 调用UserDao层查询用户,虽然只有一行代码,但是也必须写在UserService中!
return userDao.findUserById(id);
}
然后需要跟数据库打交道,新增UserDao中的findUserById
方法:
// 通过id获取用户
public User findUserById(Integer id) {
String sql = "select id,username,password,profile,dept_id from user where id = ?";
PreparedStatement statement = null;
Connection conn = getConn();
ResultSet resultSet = null;
try {
statement = conn.prepareStatement(sql);
statement.setInt(1, id);
resultSet = statement.executeQuery();
while (resultSet.next()) {
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String profile = resultSet.getString("profile");
int deptId = resultSet.getInt("dept_id");
User user = new User(username, password, profile);
user.setId(id);
user.setDeptId(deptId);
return user;
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, resultSet);
}
return null;
}
这样一套组合下来,我们就实现了根据user/update
下携带用户id的GET请求,从数据库查询该用户的信息,把该用户信息放进请求域中转发给/WEB-INF/pages/user/update.jsp
路径下的显示界面去做回显功能的实现。
到现在为止已经好多处都是这么一套流程了,跟数据库打交道的业务逻辑都是这么一层流程,这也就是MVC的思想所在,好好理解学习!
我们现在该写一个jsp页面来处理我们用户修改信息的需求,目前我们已经实现了从数据库查询要修改用户的全部数据,然后放进请求域中转发到这个jsp。
我们这个jsp需要做哪些工作呢?
我们首先要实现一个用户信息的回显功能,即拿到请求域中用户的信息显示出来;然后还要提供修改的地方让用户输入更新的信息;最后还需要提供一个按钮来发送一个POST请求携带更新后的信息。
我们先写出update.jsp
页面来实现用户回显的功能,顺便再增加一个部门的下拉选择,具体样式查询css网站去cv。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
用户修改页面
用户信息修改
我们想新增一个部门信息,能够在修改用户信息的时候选择其所在的部门。我们首先应该在update.jsp
页面之前往请求域中存入部门的信息,这样我们才能在update.jsp
中的下拉显示中显示出部门的信息。
所以我们首先要修改我们的上一层UserUpdateServlet
的业务逻辑,即在实现用户信息回显的时候,也读取数据库中的部门信息存入请求域。
@WebServlet("/user/update")
public class UserUpdateServlet extends HttpServlet {
UserService userService = new UserService();
DeptService deptService = new DeptService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 从参数中获取id
String id = req.getParameter("id");
// 根据id查询用户
User user = userService.findUserById(Integer.parseInt(id));
// 查询所有部门
List<Dept> depts = deptService.getAllDept();
// 数据存入请求域
req.setAttribute("user", user);
req.setAttribute("depts", depts);
// 转发给user.jsp
req.getRequestDispatcher("/WEB-INF/pages/user/update.jsp").forward(req, resp);
}
}
这一下子爆出来好多问题,我们没有Dept这个部门对象来储存部门的信息;没有DeptService来处理部门的下层业务;更没有DeptDao来给我们的数据库打交道。
首先建立一个Dept类用来存储部门信息,要注意:只要是存数据的类,就有可能被传输和实例化,就要实现Serializable接口。
/**
* 只要是存数据的类,就有可能被传输和实例化,就要实现Serializable接口
*/
public class Dept implements Serializable {
private static final Long serialVersionUID = 1L;
private Integer id;
private String name;
......
下面是一些构造方法和我们的getter and setter,省略了。
然后我们建立DeptService。
public class DeptService {
DeptDao deptDao = new DeptDao();
public List<Dept> getAllDept() {
return deptDao.getAllDept();
}
}
建立DeptDao来与数据库打交道,别忘了继承BaseDao来获取与数据库连接的方法。
public class DeptDao extends BaseDao {
// 获取所有的部门
public List<Dept> getAllDept() {
String sql = "select id,name from dept";
PreparedStatement statement = null;
Connection conn = getConn();
ResultSet resultSet = null;
// 存放取出的user对象
List<Dept> depts = new ArrayList<>();
try {
statement = conn.prepareStatement(sql);
resultSet = statement.executeQuery();
while (resultSet.next()) {
Integer id = resultSet.getInt("id");
String name = resultSet.getString("name");
Dept dept = new Dept(id, name);
depts.add(dept);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, resultSet);
}
return depts;
}
}
这样我们就完成了部门的大体搭建。
现在我们在update.jsp
中可以从请求域获得部门的信息了,我们接下来就可以正确使用下拉栏显示出我们所有的部门名字了,以供用户选择。
顺便要在提交POST表单的时候加一个隐藏域把我们用户的id也一并提交,方便后面修改用户信息的时候使用到用户id。
修改如下所示:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--导入JSTL标签库--%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
用户修改页面
用户信息修改
目前我们的交互界面已经完成了,点击提交这个页面会提交一个user/update
的POST请求给服务器来更新用户信息。接下来就该处理这个页面提交的更新请求了,这就是另一个servlet的事情了。
我们需要新写一个Servlet来处理user/update
的POST请求业务逻辑。
之前我们写了一个UserUpdateServlet
来处理user/update
下的GET请求,主要功能是通过请求中 用户id 来查询 数据库对应id的用户信息 和 数据库中全体部门 的信息 存入请求域中,然后把这个请求转发到update.jsp
中进行用户数据的更新。
然而现在用户在update.jsp
中更新完数据后又会往user/update
下发送一个POST请求,所以我们又应该在UserUpdateServlet
中再重写他的doPost方法,来实现对应的业务逻辑。(这块有点难想,多理解理解)
UserUpdateServlet
代码更新如下:
@WebServlet("/user/update")
@MultipartConfig
public class UserUpdateServlet extends HttpServlet {
UserService userService = new UserService();
DeptService deptService = new DeptService();
/**
* 通过id获取对应的用户信息和全体部门信息;再把请求转发到用户更新界面update.jsp。
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 从参数中获取id
String id = req.getParameter("id");
// 根据id查询用户
User user = userService.findUserById(Integer.parseInt(id));
// 查询所有部门
List<Dept> depts = deptService.getAllDept();
// 数据存入请求域
req.setAttribute("user", user);
req.setAttribute("depts", depts);
// 转发给user.jsp
req.getRequestDispatcher("/WEB-INF/pages/user/update.jsp").forward(req, resp);
}
/**
* 根据参数信息更新对应用户的信息
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取数据
String username = req.getParameter("username");
String password = req.getParameter("password");
String deptId = req.getParameter("deptId");
String id = req.getParameter("id");
// 用于存放文件的部分
Part profile = null;
InputStream inputStream = null;
// 获取文件的部分不能直接调用req.getPart()方法,必须先告诉这个servlet接受的请求是多部分的传输格式(通过注解的方式(@MultipartConfig)告诉servlet)。
try {
profile = req.getPart("profile");
inputStream = profile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (profile == null || inputStream == null) {
throw new RuntimeException("您必须上传头像!");
}
// 需要使用UUID进行处理,使得文件名不可能重复,防止覆盖掉用户头像信息
String fileName = UUID.randomUUID().toString() + "_" + profile.getSubmittedFileName();
// 需求建立不同文件夹来存储图片
String format = DateUtil.getNowString();
String path = Constant.BASEPATH + format;
// 如果路径不存在,就创建路径
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
// 通过自己定义的工具类copy文件
IOUtil.copy(inputStream, path + "/" + fileName);
String profileUrl = Constant.BASE_URL_PATH + format + "/" + fileName;
// 封装一个完整的用户
User user = new User(username, password, profileUrl);
user.setId(Integer.parseInt(id));
user.setDeptId(Integer.parseInt(deptId));
// 去捕获用户存在的异常
try {
userService.update(user);
resp.sendRedirect(req.getContextPath() + "/pages/success.jsp");
} catch (Exception e) {
e.printStackTrace();
try {
resp.sendRedirect(req.getContextPath() + "/pages/error.jsp");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
接下来干的事情用屁股想也知道,还是我们之前那一套跟数据库打交道的熟悉流程啊,写userService的update方法:
public void update(User user) {
Integer rows = userDao.update(user);
}
再写UserDao的update方法:
public Integer update(User user) {
String sql = "update user set username = ?,profile = ?,dept_id = ? where id = ?";
PreparedStatement statement = null;
Connection conn = getConn();
try {
statement = conn.prepareStatement(sql);
statement.setString(1, user.getUsername());
statement.setString(2, user.getProfile());
statement.setInt(3, user.getDeptId());
statement.setInt(4, user.getId());
// 受影响的行数
return statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, null);
}
return 0;
}
目前大BUG:提交的时候不选择头像的话,会默认把用户原有的头像删除。
自己下来实现修改一下吧!不过咱们默认不上传头像的话也是会生成一个空文件的,所以不太好实现。
所以要首先修改一下注册和更新中给文件使用UUID起名字的代码,要加入判断,如果上传文件为空,就不给注册的用户profile属性赋值,数据库中的profile应为空!
保证数据库如果没有头像存在的profile为空的情况下,再去给sql增加一些判断,如果获取的用户profile为空,则修改的时候不修改他的profile,这样就行了。不过实际实现起来还是很麻烦的。
我们发现,新增完部门信息后,在查询用户信息的界面,部门id显示的是数字,这个很不方便,我们应该让它显示对应的部门名称,这样才更好。
思路:我们应该用多表查询的语句去查询对应的部门名称
这个事情有两种处理方案可以解决。
我们可以给User中添加部门名称的属性,在展示的时候可以从user中获取。
更新UserDao的findAllUsers方法,采用多表联查的方式查询出我们所需要的部门名称数据,存入user对象中。
// 获取所有的用户
public List<User> findAllUsers() {
String sql = "select u.id,u.username,u.password,u.profile,u.dept_id,d.name from user u left join dept d on u.dept_id = d.id";
PreparedStatement statement = null;
Connection conn = getConn();
ResultSet resultSet = null;
// 存放取出的user对象
List<User> users = new ArrayList<>();
try {
statement = conn.prepareStatement(sql);
resultSet = statement.executeQuery();
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String profile = resultSet.getString("profile");
int deptId = resultSet.getInt("dept_id");
String deptName = resultSet.getString("name");
User user = new User(username, password, profile);
user.setId(id);
user.setDeptId(deptId);
user.setDeptName(deptName);
users.add(user);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, resultSet);
}
return users;
}
给User对象新增属性,更改Uer类。当然要记得写deptName的getter和setter方法。
/**
* 只要是存数据的类,就有可能被传输和实例化,就要实现Serializable接口
*/
public class User implements Serializable {
private static final Long serialVersionUID = 1L;
private Integer id;
private String username;
private String password;
private String profile;
private Integer deptId;
private String deptName;
......
修改展示页面的部门数据源(user.jsp
)
已注册用户信息展示
id
用户名
头像
部门
操作
<%--使用JSTL标签库从请求域中循环遍历拿到每一个用户--%>
${user.id}
${user.username}
${user.deptName}
修改
我们还可以给User中添加一个此用户的部门对象,把有关部门的所有信息都存进去。这种方法用的会多很多,因为前一种部门里每多一个属性你User里就要多一个属性,很难看也很冗余,是个偷懒的办法。
我们更改User中的属性:
/**
* 只要是存数据的类,就有可能被传输和实例化,就要实现Serializable接口
*/
public class User implements Serializable {
private static final Long serialVersionUID = 1L;
private Integer id;
private String username;
private String password;
private String profile;
// 用户所在的部门
private Dept dept;
......
然后之前写点好多有部门id的地方都要进行修改,基本都在UserDao、UserUpdateServlet中,我们修改的逻辑都一样,都是把user.setDeptId(deptId)
改为封装一个Dept对象,传入进去。
Dept dept = new Dept();
dept.setId(deptId);
user.setDept(dept);
最后,把我们显示的查询用户页面(user.jsp
)的部门名称的数据源改一下,从user.deptId
改为user.dept.name
。
<%--使用JSTL标签库从请求域中循环遍历拿到每一个用户--%>
${user.id}
${user.username}
${user.dept.name}
修改
这样就实现了我们部门名称的正常展示。
如果用户非常多的话,不可能在一页内展示那么多数据吧,所以说我们应该在用户展示jsp实现分页功能。
先在网站里找到我们需要的css分页组件。
首先应该在user.jsp
中加入我们找到的css分页组件,在table后面:
<table class="table table-hover">
<thead>
<tr>
<td>id</td>
<td>用户名</td>
<td>头像</td>
<td>部门</td>
<td>操作</td>
</tr>
</thead>
<%--使用JSTL标签库从请求域中循环遍历拿到每一个用户--%>
<c:forEach items="${requestScope.users}" var="user">
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td><img width="30px" src="${user.profile}"></td>
<td>${user.dept.name}</td>
<td>
<button data-id="${user.id}" type="button" class="delete btn btn-primary">删除</button>
<a href="user/update?id=${user.id}" type="button" class="btn btn-primary">修改</a>
</td>
</tr>
</c:forEach>
</table>
<%--分页组件--%>
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<li class="page-item"><a class="page-link" href="user?currentPage=1&pageSize=5">1</a></li>
<li class="page-item"><a class="page-link" href="user?currentPage=2&pageSize=5">2</a></li>
<li class="page-item"><a class="page-link" href="user?currentPage=3&pageSize=5">3</a></li>
<li class="page-item">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
我们先按5个用户为一组分3页写死,等后面再优化。我们点击页数的时候会向/user
发送一个携带当前页面数和页面条数的get请求,接下来我们就需要根据这个请求去更改我们的业务逻辑了,要用到分页查询的sql了。
首先我们更改UserServlet:
@WebServlet("/user")
public class UserServlet extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 当前页和每页的条数
Integer currentPage = req.getParameter("currentPage") == null ? 1
: Integer.parseInt(req.getParameter("currentPage"));
Integer pageSize = req.getParameter("pageSize") == null ? 5
: Integer.parseInt(req.getParameter("pageSize"));
// 查询指定页数和当前页数条目范围内的用户
List<User> users = userService.findAllUsers(currentPage, pageSize);
// 转发给user.jsp
req.setAttribute("users", users);
req.getRequestDispatcher("/WEB-INF/pages/user/user.jsp").forward(req, resp);
}
}
我们更改了UserService
的findAllUsers
方法,加入两个参数传进去,还是老样子,用屁股想都知道该改UserService和UserDao了。
更改UserService
的findAllUsers
方法:
// 调用UserDao层的方法找指定的用户
// 一定要注意MVC的思想,虽然只有一行代码,但是也必须写在UserService中,不能直接去调用Dao来完成操作!!必须由UserService再去找Dao操作才合理!
public List<User> findAllUsers(Integer currentPage, Integer pageSize) {
return userDao.findAllUsers(currentPage, pageSize);
}
更改UserDao
的findAllUsers
方法:
// 获取指定的用户
public List<User> findAllUsers(Integer currentPage, Integer pageSize) {
String sql = "select u.id,u.username,u.password,u.profile,u.dept_id,d.name from user u left join dept d on u.dept_id = d.id limit ?,?";
PreparedStatement statement = null;
Connection conn = getConn();
ResultSet resultSet = null;
// 存放取出的user对象
List<User> users = new ArrayList<>();
try {
statement = conn.prepareStatement(sql);
statement.setInt(1, (currentPage - 1) * pageSize);
statement.setInt(2, pageSize);
resultSet = statement.executeQuery();
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String profile = resultSet.getString("profile");
int deptId = resultSet.getInt("dept_id");
String deptName = resultSet.getString("name");
User user = new User(username, password, profile);
user.setId(id);
Dept dept = new Dept(deptId, deptName);
user.setDept(dept);
users.add(user);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, resultSet);
}
return users;
}
这样我们就基本完成了分页展示的雏形了,剩下一些功能的完善工作。
首先要获取数据库一共有多少条目的数据,才能显示出来页数按钮最大到多少。
怎么知道当前一共有多少页呢?还得去靠数据库查询,又是一套老流程…用屁股想也知道了吧
首先我们要更新UserServlet中的doGet方法,获取currentPage、pageSize和totalPage传入请求域中。
@WebServlet("/user")
public class UserServlet extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 当前页和每页的条数
Integer currentPage = req.getParameter("currentPage") == null ? 1
: Integer.parseInt(req.getParameter("currentPage"));
Integer pageSize = req.getParameter("pageSize") == null ? 5
: Integer.parseInt(req.getParameter("pageSize"));
// 查询指定页数和当前页数条目范围内的用户
List<User> users = userService.findAllUsers(currentPage, pageSize);
// 查询数据库总数目条数
Integer total = userService.getUserTotal();
req.setAttribute("users", users);
// 把当前页和共多少页的参数传到请求域中,方便jsp文件中实现上下页功能
req.setAttribute("currentPage", currentPage);
req.setAttribute("pageSize", pageSize);
// 根据total条目直接计算出总页数,在传入请求域中。计算公式:total转doble类型 除以 pageSize 后 向上去整。
req.setAttribute("totalPage", (int) Math.ceil(((double) total) / pageSize));
// 转发给user.jsp
req.getRequestDispatcher("/WEB-INF/pages/user/user.jsp").forward(req, resp);
}
}
其中total要靠数据库去查询,写那一套用屁股都能想出来的方法。currentPage和pageSize就要从参数中去获取了,这个存入请求域是为了实现上下页功能,上下页就可以从请求域中直接获取当前页来进行加减1的操作实现翻页。
接下来就是用屁股都能想出来的一整套连招,来获取total。
UserService:
public Integer getUserTotal() {
return userDao.getUserTotal();
}
UserDao:
// 查询用户的总数
public Integer getUserTotal() {
String sql = "select count(*) total from user";
PreparedStatement statement = null;
Connection conn = getConn();
try {
statement = conn.prepareStatement(sql);
// 受影响的行数
ResultSet resultSet = statement.executeQuery();
resultSet.next();
return resultSet.getInt("total");
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeAll(conn, statement, null);
}
return 0;
}
最后我们来更新一下我们的user.jsp
文件,实现用总数目算出总页数,并输出对应页数条目的功能。顺便也要把加减页的操作在jsp中补完。
分页组件更新如下:
<%--分页组件--%>
这样我们就实现了动态显示分页功能,但是还有问题,如果真有100页要显示,要显示100页吗?
我们还需要改进功能,我们不可能有多少页就全部显示出来吧,我想实现一个功能,只能动态显示最近的5页出来,多余的不显示。
实现显示最近5页的功能很简单,改一下for循环里的起始页数就好了,把循环次数定死为5次。
<%--分页组件--%>
但是这样子会显示超出范围,我们还在做出处理,让它显示的别超出范围。在for里加一个if判断就行了,如果范围不满足,就不显示出来。
<%--分页组件--%>
这样就彻底完善了我们的用户的分页显示。
我们采取把当前页面封装成一个对象的思路去实现异步刷新。
首先我们要创建一个Page类,存放一个页面的信息,给两个属性,total代表信息总数目,再来一个泛型集合用于存放数据。
public class Page<T> implements Serializable {
private static final Long serialVersionUID = 1L;
// 数据的总条数
private Integer total;
// 数据
private List<T> data;
// 当前页
private Integer currentPage;
// 每一页条数
private Integer pageSize;
......
我们就应该写异步所对应的servlet了,就用/user2
吧。
/**
* 使用异步局部刷新的方式处理用户列表
*/
@WebServlet("/user2")
public class User2Servlet extends HttpServlet {
UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 当前页和每页的条数
Integer currentPage = req.getParameter("currentPage") == null ? 1
: Integer.parseInt(req.getParameter("currentPage"));
Integer pageSize = req.getParameter("pageSize") == null ? 5
: Integer.parseInt(req.getParameter("pageSize"));
// 查询指定页数和当前页数条目范围内的用户
List<User> users = userService.findAllUsers(currentPage, pageSize);
// 查询数据库总数目条数
Integer total = userService.getUserTotal();
// 封装分页对象
Page<User> page = new Page<>(total, users);
page.setPageSize(pageSize);
page.setCurrentPage(currentPage);
// 转变成json格式给前端传过去,用到fastjson小组件
String pageStr = JSONObject.toJSONString(page);
resp.setHeader("Content-Type", "application/json;charset=utf-8");
resp.getWriter().write(pageStr);
resp.getWriter().flush();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("/WEB-INF/pages/user/user2.jsp").forward(req, resp);
}
}
这里我们是想把封装好的Page对象传给前端去实现异步更新,采取ajax来实现异步操作。
但是要注意,Page是一个对象,使用流的方式去序列化Page对象传到前端,前端肯定是不识别的。之前在采取同步的时候,为什么用jsp可以识别呢?因为jsp本质就是一个servlet,它处理的时候肯定后台要把它转换成一个servlet对象,当然可以识别。但是前端是没有办法识别出Page对象的!我们采取流的形式给前端传过去是不可能的。
所以我们需要把这个Page转换成一个json字符来传给前端,并且让前端可以识别到是一个json,这个样就可以去打印了。
这里就用到了一个小组件:fastjson,它可以实现json的序列化功能,是一个阿里的员工写的。他可以帮我们把Page转换成一个json字符串来传给前端。
现在我们的逻辑是这样的:
/user2
就会跳转到user2.jsp页面下展示分页用户信息;/user2
发送POST请求,Servlet根据POST请求的参数查好对应用户信息,把所需要的用户信息通过json字符串包装成响应信息发给前端;现在理清逻辑后,我们该写user2.jsp
页面了,需要使用ajax实现点击按钮发送的POST请求,和接收到对应响应后做相应的 字符串拼接操作 使用异步把信息展示出来。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--导入JSTL标签库--%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
用户展示页面-异步
已注册用户信息-异步
id
用户名
头像
部门
操作
<%--分页组件--%>
这里尤为要注意一点,ajax的执行顺序特别重要。
我们一旦想使用ajax来监听某个按钮或者事件,要先保证这个事件存在!举个例子,我们使用异步的方式局部刷新表单,我们的监听必须等到服务器的响应被接收处理后再进行监听处理,因为按钮的创建时间是在拿到响应之后。我们如果最开始先监听再创建按钮,那怎么可能监听到呢?
同步是浏览器给服务器发送请求,服务器收到并处理请求,然后给浏览器发送一个处理好的html页面来显示所需要的信息。
异步是浏览器给服务器发送请求,服务器收到并处理请求,然后给浏览器发送个空页面带上一串js脚本。浏览器收到后拿到脚本中所需要的信息,然后通过dom表单的形式更新部分元素(ajax)。
做的简陋一点
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
首页
欢迎${sessionScope.user.username==null?"":sessionScope.user.username}登陆!
记得把登陆的Servlet中登陆成功的跳转换成刚做的主页
添加白名单、老一套,自己完成
@WebFilter("/*")
public class LoginFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//创建白名单
List<String> witheNames = new ArrayList<>();
witheNames.add(request.getContextPath() + "/index.jsp");
witheNames.add(request.getContextPath() + "/user/login");
witheNames.add(request.getContextPath() + "/login.jsp");
witheNames.add(request.getContextPath() + "/user/register");
witheNames.add(request.getContextPath() + "/register.jsp");
witheNames.add(request.getContextPath() + "/pages/error.jsp");
witheNames.add(request.getContextPath() + "/pages/success.jsp");
witheNames.add(request.getContextPath() + "/user/getProfile");
witheNames.add(request.getContextPath() + "/user/checkUserName");
witheNames.add(request.getContextPath() + "/user/verification");
// 如果在白名单我就放行
if (witheNames.contains(request.getRequestURI())) {
chain.doFilter(request, response);
} else {
HttpSession session = request.getSession(false);
// 有用户信息说明已经登录
if (session != null && session.getAttribute("user") != null) {
chain.doFilter(request, response);
} else {
response.sendRedirect(request.getContextPath() + "/login.jsp");
}
}
}
}
给xml配置文件配置我们常量类里的常量,用context-param来配置:
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0"
metadata-complete="false">
<context-param>
<param-name>SALTparam-name>
<param-value>!@#hjjkh!H!JKHparam-value>
context-param>
<context-param>
<param-name>BASE_PATHparam-name>
<param-value>E://Java/Projects/learnTomcat/user-manager/www/img/param-value>
context-param>
<context-param>
<param-name>BASE_URL_PATHparam-name>
<param-value>http://localhost:8080/image/param-value>
context-param>
web-app>
创建监听器,在服务器启动的时候把配置的参数赋值给常量类。
@WebListener
public class ContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext servletContext = sce.getServletContext();
Constant.SALT = servletContext.getInitParameter("SALT");
Constant.BASE_PATH = servletContext.getInitParameter("BASE_PATH");
Constant.BASE_URL_PATH = servletContext.getInitParameter("BASE_URL_PATH");
}
}
1、配置一个产品,我们的项目构建打包后就是一个产品
2、选择web application:archive,它会帮助我们制作一个war包。
3、点击项目构建build
4、选择build artifact,点击build
5、最终的产品就会出现在out目录
6、将war包放在tomcat的webapp下启动即可。
我们发现头像不能正常显示了,这是因为新打包好的项目没有添加图片的虚拟路径,我们需要在tomcat的config下找到server.xml里进行配置。
配置虚拟路径可以帮我们搭建一个简易的图片服务器,让我们上传的图片可以用url访问。
<Context path="/image" docBase="E://Java/Projects/learnTomcat/user-manager/www/img/" debug="0" reloadbale="true"/>
path: Host的虚拟目录 docBase: 映射的物理目录的地址,可指定相对路径,相对appBase下,也可以指定绝对路径(例如:D:\Workes\testtomcat\WebRoot)。如果无此项则默认为appBase/ROOT 。
还有很多功能可以我们下来自己进行完善: