通过 Servlet + JDBC +Cookie + Session + JSP + HTML 实现一个博客系统 后端开发
其实算不上是一个项目,就是细细碎碎将Servlet开发Web程序的所有点走了一遍,功能比较简单,但是很全面,基本上所有的点都覆盖了,整理到头秃的一篇博客,目的在于学会使用如何使用Servlet进行Web程序的开发
整体设计
1. 用户列表
1.1 功能列表
1)注册用户——信息来自form提交
2)用户登录——信息来自form提交
3)查看用户信息——username从url中获取
4)修改用户信息(前提是已经登录)—— 展示当前信息、修改后刷新的信息
5)通用功能:考虑基本的安全性
1.2. 库表设计
属性: id / username / password / nickname / brief / registered_at
CREATE DATABASE java20_blog04 charset utf8mb4;
USE java20_blog04;
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(100) NOT NULL UNIQUE,
password CHAR(64) NOT NULL,
nickname CHAR(64) NOT NULL,
brief VARCHAR(200) NOT NULL,
registered_at DATETIME NOT NULL
);
1.3 整理功能需要的 页面资源(API设计)
1)注册用户
GET / register,html 静态
POST/reg 动态
2)登录
GET / login.html 静态
POST / log 动态
3)获取用户信息
GET / u? username = … 动态
4)修改用户个人资料
GET / profile 动态
POST / profile / edit 动态
<一> 注册:
一、目录结构
二、静态页面
注册静态页面 register.html
!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册</title>
</head>
<body>
<form action="/reg" method="post">
<div>
用户名<input type="text" name="username" />
</div>
<div>
昵称<input type="text" name="nickname" />
</div>
<div>
密码<input type="password" name="password" />
</div>
<div>
<button type="submit">注册</button>
</div>
</form>
</body>
</html>
登录静态页面 login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="/log" method="post">
<div>
用户名<input type="text" name="username" />
</div>
<div>
密码<input type="password" name="password" />
</div>
<div>
<button type="submit">登录</button>
</div>
</form>
</body>
</html>
三、动态代码
注册 register.java
package com.peixinchen.servlets;
import com.peixinchen.model.User;
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.io.PrintWriter;
import java.sql.SQLException;
@WebServlet("/reg")
public class RegisterServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//首先,从请求中获取用户提交的信息(包括用户名、昵称、密码)
req.setCharacterEncoding("utf-8"); //按照UTF-8编码形式,从 请求中 读取用户的提交内容
String username = req.getParameter("username");
String nickname = req.getParameter("nickname");
String password = req.getParameter("password");
//对用户输入的内容进行合法性校验——永远不要完全相信用户的输入
if (username == null || username.isEmpty()){
System.out.println("用户名不合法!");
resp.sendRedirect("/register.html");//重新回到注册页面
return;
}
if (nickname == null || nickname.isEmpty()){
System.out.println("昵称不合法!");
resp.sendRedirect("/register.html");//重新回到注册页面
return;
}
if (password == null || password.isEmpty()){
System.out.println("密码不合法!");
resp.sendRedirect("/register.html");//重新回到注册页面
return;
}
//进行用户的注册过程
try {
User user = User.register(username,nickname,password);
System.out.println(user);
}catch (SQLException e){
e.printStackTrace();
throw new ServletException(e);
}
//给出成功信息
resp.setContentType("text/html");
resp.setCharacterEncoding("utf-8");
PrintWriter writer = resp.getWriter();
writer.println("<h1>注册成功!</h1>");
}
}
用户 User.java
package com.peixinchen.model;
import com.peixinchen.util.Database;
import java.sql.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class User {
public int id;
public String username;
public String nickname;
public String brief;
public Date registered_at;
public User(int id, String username, String nickname, String brief, Date registeredAt) {
this.id = id;
this.username = username;
this.nickname = nickname;
this.brief = brief;
this.registered_at = registeredAt;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", nickname='" + nickname + '\'' +
", brief='" + brief + '\'' +
", registeredAt=" + registered_at +
'}';
}
public static User register(String username, String nickname, String password) throws SQLException {
int id;
String brief = "这个人很懒,什么都没有留下~";
Date registered_at = new Date();
DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//SQL
String sql = "INSERT INTO users (username,password,nickname,brief,registered_at) VALUES (?,?,?,?,?)";
try (Connection con = Database.getConnection()){
try (PreparedStatement stmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)){
//Statement.RETURN_GENERATED_KEYS 目的是获取插入后的自增id
stmt.setString(1,username);
stmt.setString(2,password);
stmt.setString(3,nickname);
stmt.setString(4,brief);
stmt.setString(5,fmt.format(registered_at));
stmt.executeUpdate();
//插入成功后就可以通过 getGeneratedKeys() 获取自增id
try (ResultSet rs = stmt.getGeneratedKeys()){
if (rs.next()){
id = rs.getInt(1);
return new User(id,username,nickname,brief,registered_at);
}
}
}
}
return null;
}
}
四、运行结果
点击注册:
无视飘红哈,可以看到已经注册成功了
mysql也已经成功插入:
五、遇到的问题和解决办法
1、405错误
解决办法:
因为代码中用到的是POST,所以只支持POST,但是浏览器发来了GET,这里是不允许直接通过URL访问的
所以需要访问 register.html,登陆成功后会跳转到 reg
通过点击注册进行页面跳转
2、500错误
注册页面可以正常注册,注册完成后点击提交出现下图错误
从上面报错信息可以看出是sql的jar包导入有问题,最后得知是因为sql的jar包没有加入到artifacts里面:
<二> 对用户的密码进行哈希
(上面的方法可以看到对用户的密码是明文保存的,明显是不可取的,下面就是对密码进行哈希后存进数据库)
拓展:
清掉原有的表数据命令行:
一、哈希方法
User.java 整体代码:
package com.peixinchen.model;
import com.peixinchen.util.Database;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class User {
public int id;
public String username;
public String nickname;
public String brief;
public Date registered_at;
public User(int id, String username, String nickname, String brief, Date registeredAt) {
this.id = id;
this.username = username;
this.nickname = nickname;
this.brief = brief;
this.registered_at = registeredAt;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", nickname='" + nickname + '\'' +
", brief='" + brief + '\'' +
", registeredAt=" + registered_at +
'}';
}
public static User register(String username, String nickname, String password) throws SQLException {
//把密码 Hash 后保存进数据库,防止因为数据库被拖库进而导致的用户密码泄露
//现在常用的哈希方法:SHA256
password = hash(password);
int id;
String brief = "这个人很懒,什么都没有留下~";
Date registered_at = new Date();
DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//SQL
String sql = "INSERT INTO users (username,password,nickname,brief,registered_at) VALUES (?,?,?,?,?)";
try (Connection con = Database.getConnection()){
try (PreparedStatement stmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)){
//Statement.RETURN_GENERATED_KEYS 目的是获取插入后的自增id
stmt.setString(1,username);
stmt.setString(2,password);
stmt.setString(3,nickname);
stmt.setString(4,brief);
stmt.setString(5,fmt.format(registered_at));
stmt.executeUpdate();
//插入成功后就可以通过 getGeneratedKeys() 获取自增id
try (ResultSet rs = stmt.getGeneratedKeys()){
if (rs.next()){
id = rs.getInt(1);
return new User(id,username,nickname,brief,registered_at);
}
}
}
}
return null;
}
private static String hash(String password) {
try {
StringBuilder sb = new StringBuilder();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encrypted = digest.digest(password.getBytes("UTF-8"));
for (byte b : encrypted) {
sb.append(String.format("%02x",b));
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
return password;
}
}
}
再次进行注册,可以看到密码已经进行了哈希:
二、密码的安全处理
!! 如果将安全级别分为1~10分,我这里使用的安全级别仅仅为2分
数据库中保存的密码 = HASH(原始密码)
使用的 HASH 算法是 SHA-256 算法
而哈希是有碰撞的,可能不同的原始密码得到相同的哈希结果,因为有这种可能性。所以这个方法只能从左往右走,不能从右往左走,所以这就体现了安全性,即使知道了数据库中保存的密码,也不能得到真正的密码
<三> 查看用户信息
一、代码
查看用户信息 UserServlet.java
package com.peixinchen.servlets;
import com.peixinchen.model.User;
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.io.PrintWriter;
import java.sql.SQLException;
import java.text.ParseException;
@WebServlet("/u")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
// TODO:增加合法性校验
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.println("<!DOCTYPE html>");
writer.println("<html lang=\"en\">");
writer.println("<head>");
writer.println("<meta charset='UTF-8'>");
writer.println("<title>个人主页</title>");
writer.println("</head>");
writer.println("<body>");
//根据用户名查看user信息
User user = null;
try {
user = User.getByUsername(username);
} catch (SQLException | ParseException e) {
e.printStackTrace();
throw new ServletException(e);
}
if (user == null){
resp.setStatus(404);
writer.println("<h1>没有这个用户: "+username+"</h1>");
}else {
writer.println("<h1>" + user.nickname + "</h1>");
writer.println("<p>" + user.id + "</p>");
writer.println("<p>" + user.username + "</p>");
writer.println("<p>" + user.brief + "</p>");
writer.println("<p>" + user.getRegisteredAt() + "</p>");
}
writer.println("</body>");
writer.println("</html>");
}
}
用户User.java:
package com.peixinchen.model;
import com.peixinchen.util.Database;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.*;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class User {
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public int id;
public String username;
public String nickname;
public String brief;
private Date registered_at;
public String getRegisteredAt(){
return dateFormat.format(registered_at);
}
public User(int id, String username, String nickname, String brief, Date registeredAt) {
this.id = id;
this.username = username;
this.nickname = nickname;
this.brief = brief;
this.registered_at = registeredAt;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", nickname='" + nickname + '\'' +
", brief='" + brief + '\'' +
", registeredAt=" + registered_at +
'}';
}
public static User register(String username, String nickname, String password) throws SQLException {
//把密码 Hash 后保存进数据库,防止因为数据库被拖库进而导致的用户密码泄露
//现在常用的哈希方法:SHA256
password = hash(password);
int id;
String brief = "这个人很懒,什么都没有留下~";
Date registered_at = new Date();
//SQL
String sql = "INSERT INTO users (username,password,nickname,brief,registered_at) VALUES (?,?,?,?,?)";
try (Connection con = Database.getConnection()){
try (PreparedStatement stmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)){
//Statement.RETURN_GENERATED_KEYS 目的是获取插入后的自增id
stmt.setString(1,username);
stmt.setString(2,password);
stmt.setString(3,nickname);
stmt.setString(4,brief);
stmt.setString(5,dateFormat.format(registered_at));
stmt.executeUpdate();
//插入成功后就可以通过 getGeneratedKeys() 获取自增id
try (ResultSet rs = stmt.getGeneratedKeys()){
if (rs.next()){
id = rs.getInt(1);
return new User(id,username,nickname,brief,registered_at);
}
}
}
}
return null;
}
//实际上也就是通过sql实现
public static User getByUsername(String username) throws SQLException, ParseException {
String sql = "SELECT id,nickname,brief,registered_at FROM users WHERE username = ?";
User user = null;
try (Connection con = Database.getConnection()){
try (PreparedStatement stmt = con.prepareStatement(sql)){
stmt.setString(1,username);
try (ResultSet rs = stmt.executeQuery()){
if (rs.next()){
int id = rs.getInt(1);
String nickname = rs.getString(2);
String brief = rs.getString(3);
String registeredAtStr = rs.getString(4);
Date registered_at = dateFormat.parse(registeredAtStr);
user = new User(id,username,nickname,brief,registered_at);
}
}
}
}
return user;
}
//对密码进行哈希
private static String hash(String password) {
try {
StringBuilder sb = new StringBuilder();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encrypted = digest.digest(password.getBytes("UTF-8"));
for (byte b : encrypted) {
sb.append(String.format("%02x",b));
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
return password;
}
}
}
二、运行结果
三 、代码分析
- 第一步从 querySting中拿到用户名,也就是url中
- 打印布局
- 从数据库获取用户信息
- 输出信息
<四> 查看用户信息的改进
如果用户没有传入用户名,则获取当前登录用户的信息
一、代码
查看用户信息 UserServlet.java
package com.peixinchen.servlets;
import com.peixinchen.model.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.text.ParseException;
@WebServlet("/u")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
// TODO:增加合法性校验
//如果没有传入用户名,则返回当前登录用户的信息
//1.怎么获取当前用户(Cookie + Session)
if (username == null){
HttpSession session = req.getSession();
User user = (User) session.getAttribute("user");
if (user != null){//说明用户登录过了
printHtmlUser(user,resp);
} else {
print404(resp,"必须传入用户名!");
}
return;
}
//根据用户名查看user信息
User user;
try {
user = User.getByUsername(username);
} catch (SQLException | ParseException e) {
e.printStackTrace();
throw new ServletException(e);
}
if (user == null){
print404(resp,username);
}else {
printHtmlUser(user,resp);
}
}
//打印用户主页
private void printHtmlUser(User user,HttpServletResponse resp) throws IOException {
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.println("<!DOCTYPE html>");
writer.println("<html lang=\"en\">");
writer.println("<head>");
writer.println("<meta charset='UTF-8'>");
writer.println("<title>个人主页</title>");
writer.println("</head>");
writer.println("<body>");
writer.println("<h1>" + user.nickname + "</h1>");
writer.println("<p>" + user.id + "</p>");
writer.println("<p>" + user.username + "</p>");
writer.println("<p>" + user.brief + "</p>");
writer.println("<p>" + user.getRegisteredAt() + "</p>");
writer.println("</body>");
writer.println("</html>");
}
//打印404错误
private void print404(HttpServletResponse resp,String username) throws IOException {
resp.setStatus(404);
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.println("<!DOCTYPE html>");
writer.println("<html lang=\"en\">");
writer.println("<head>");
writer.println("<meta charset='UTF-8'>");
writer.println("<title>个人主页</title>");
writer.println("</head>");
writer.println("<body>");
writer.println("<h1>没有这个用户: "+username+"</h1>");
writer.println("</body>");
writer.println("</html>");
}
}
二、运行结果
未登录情况下直接查看个人信息
<五>注册成功直接访问个人主页是没有结果的
到这里为止只是注册成功了,直接访问 /u 是没有结果的,为什么呢?
注册成功不等于登陆成功
可以看到获取用户是从Session中获取的,但是此时Session中是没有东西的
一、 将信息存入Session
所以要在注册时,注册成功后讲用户信息放入Session
二、 代码
RegisterServlet.java
package com.peixinchen.servlets;
import com.peixinchen.model.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
@WebServlet("/reg")
public class RegisterServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//首先,从请求中获取用户提交的信息(包括用户名、昵称、密码)
req.setCharacterEncoding("utf-8"); //按照UTF-8编码形式,从 请求中 读取用户的提交内容
String username = req.getParameter("username");
String nickname = req.getParameter("nickname");
String password = req.getParameter("password");
//对用户输入的内容进行合法性校验——永远不要完全相信用户的输入
if (username == null || username.isEmpty()){
System.out.println("用户名不合法!");
resp.sendRedirect("/register.html");//重新回到注册页面
return;
}
if (nickname == null || nickname.isEmpty()){
System.out.println("昵称不合法!");
resp.sendRedirect("/register.html");//重新回到注册页面
return;
}
if (password == null || password.isEmpty()){
System.out.println("密码不合法!");
resp.sendRedirect("/register.html");//重新回到注册页面
return;
}
resp.setContentType("text/html");
resp.setCharacterEncoding("utf-8");
PrintWriter writer = resp.getWriter();
//进行用户的注册过程
try {
User user = User.register(username,nickname,password);
System.out.println(user);
//给出成功信息
writer.println("<h1>注册成功!</h1>");
//登录用户,也就是把注册成功的用户放入Session中
HttpSession session = req.getSession();
session.setAttribute("user",user);//key要和UserServlet对的上
}catch (SQLException e){
e.printStackTrace();
throw new ServletException(e);
}
}
}
三、运行结果
注册:
注册成功:
访问个人用户信息:
四、 过程分析:
<六> 修改用户资料
(由于到这里还没有写登录页面,所以只能靠注册后直接到修改用户资料页面来修改)
一、修改界面代码
修改个人信息 ProfileServlet.java
package com.peixinchen.servlets;
import com.peixinchen.model.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/profile")
public class ProfileServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
User user =(User) session.getAttribute("user");
if (user == null){//说明用户没有登录
//跳转到用户登录页
resp.sendRedirect("/login.html");
return;
}
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.println("<!DOCTYPE html>");
writer.println("<html>");
writer.println(" <head>");
writer.println(" <head>");
writer.println(" <meta charset='UTF-8'>");
writer.println(" <title>个人资料修改</title>");
writer.println(" </head>");
writer.println(" <body>");
writer.println(" <form action='/profile/edit' method='post'>");
writer.println(" <div>");
writer.println(" 昵称<input type='text' name='nickname' value='" + user.nickname+"'>");
writer.println(" </div>");
writer.println(" <div>");
writer.println(" 个性签名<input type='text' name='brief' value='" + user.brief+"'>");
writer.println(" </div>");
writer.println(" <div>");
writer.println(" <button type='submit'>修改</button>");
writer.println(" </div>");
writer.println(" </form>");
writer.println(" </body>");
writer.println("</html>");
}
}
二、运行结果
注册后访问profile就可以点击输入框进行修改:
三、引入JSP:
有一种场景:资源是动态的,但HTML内容特别多,纯粹在Servlet中写是可以实现的,由下图可见,是非常的麻烦,所以引入了JSP
JSP的本质就是Servlet,只是把拼HTML的过程交给Tomcat内部去处理了
profile.jsp
<%@ page import="com.peixinchen.model.User" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
User user = (User) session.getAttribute("user");
if (user == null){
response.sendRedirect("/login.html");
return;
}
%>
<html>
<head>
<meta charset="UTF-8">
<title>个人资料修改</title>
</head>
<body>
<form action="/profile/edit" method="post">
<div>
昵称<input type="text" name="nickname" value="<%= user.nickname%>">
</div>
<div>
个性签名<input type="text" name="brief" value="<%= user.brief%>">
</div>
<div>
<button type="submit">修改</button>
</div>
</form>
</body>
</html>
运行结果:
可以看到效果是一样的:
四、修改信息
修改的是谁的信息呢?
这个时候HTTP无状态的特性就体现出来了,导致我不知道修改的是谁的信息
所以需要通过登录状态处理
通过Session获取
ProfileEditServlet.java
package com.peixinchen.servlets;
import com.peixinchen.model.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.sql.SQLException;
@WebServlet("/profile/edit")
public class ProfileEditServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
String nickname = req.getParameter("nickname");
String brief = req.getParameter("brief");
//TODO:合法性校验
HttpSession session = req.getSession();
User user =(User) session.getAttribute("user");
if (user == null){
//用户没有登录
resp.sendError(401);
return;
}
//修改用户信息,修改的是哪个用户的资料?Session
try {
User.update(user.id,nickname,brief);
} catch (SQLException e) {
e.printStackTrace();
throw new ServletException(e);
}
//修改后将Session中的也要修改
user.nickname = nickname;
user.brief = brief;
session.setAttribute("user",user);
resp.sendRedirect("/u");
}
}
User.java _ update()
//修改用户信息
public static void update(int id, String nickname, String brief) throws SQLException {
String sql = "UPDATE users SET nickname = ?, brief = ? WHERE id = ?";
try(Connection con = Database.getConnection()) {
try (PreparedStatement stmt = con.prepareStatement(sql)){
stmt.setString(1,nickname);
stmt.setString(2,brief);
stmt.setInt(3,id);
stmt.executeUpdate();
}
}
}
<七> 用户登录
一、代码
LoginServlet.java
package com.peixinchen.servlets;
import com.peixinchen.model.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
@WebServlet("/log")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
//TODO:合法性校验
try {
User user = User.login(username,password);
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
if (user != null) {
//登录成功
HttpSession session = req.getSession();
session.setAttribute("user", user);
writer.println("<h1>登陆成功!</h1>");
}else {
//登陆失败
writer.println("<h1>登陆失败!</h1>");
}
} catch (SQLException e){
throw new ServletException(e);
}
}
}
User.java _ login()
public static User login(String username, String password) throws SQLException {
String sql = "SELECT id, nickname, brief, registered_at FROM users WHERE username = ? AND PASSWORD = ?";
try (Connection con = Database.getConnection()) {
try (PreparedStatement stmt = con.prepareStatement(sql)) {
stmt.setString(1, username);
stmt.setString(2, hash(password)); // 密码在保存时已经被 Hash 过了,查找也得进行一次 Hash
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
int id = rs.getInt("id");
String nickname = rs.getString("nickname");
String brief = rs.getString("brief");
Date registeredAt = dateFormat.parse(rs.getString("registered_at"));
return new User(id, username, nickname, brief, registeredAt);
} else {
return null;
}
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
}
}
二、运行结果
<八> 文章管理
一、库表设计
头图 + 标题 + 内容
CREATE TABLE articles(
id INT PRIMARY KEY AUTO_INCREMENT,
cover_image VARCHAR(200) NOT NULL,
author_id INT NOT NULL,
title VARCHAR(200) NOT NULL,
body VARCHAR(600) NOT NULL,
published_at DATETIME NOT NULL
);
1. 如何在mysql中保存图片
1)把图片数据保存到本地磁盘上,以文件形式保存
MySQL中只保存 1.本地的路径 2.图片的URL
2)把图片数据保存到MySQL的字段中,用BLOB形式
我这里用的是的第一种,保存到本地磁盘上,用本地的路径
二、 文章发表(暂无头图)
- 通过JSP完成——form表单,允许用户填写要发表的文章信息
为什么不用静态的?因为我们要求要先登录后才有权限发表文章 - 通过Servlet实现——发表文章
writer.jsp
<%@ page import="com.peixinchen.model.User" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
User user =(User) session.getAttribute("user");
if (user == null){
//用户没有登录
response.getWriter().println("<h1>请先登录再进行操作!</h1>");
return;
}
%>
<html>
<head>
<meta charset="UTF-8">
<title>发表文章</title>
</head>
<body>
<h1>发表文章</h1>
<form action="/publish" method="post">
<div>
标题:<input type="text" name="title">
</div>
<div>
正文:<textarea name="body"></textarea>
</div>
<button type="submit">发表</button>
</form>
</body>
</html>
PublishServlet.java
package com.peixinchen.servlets;
import com.peixinchen.model.Article;
import com.peixinchen.model.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
@WebServlet("/publish")
public class PublishServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
String title = req.getParameter("title");
String body = req.getParameter("body");
HttpSession session = req.getSession();
User user =(User) session.getAttribute("user");
if (user == null){
//说明没有登录,跳转到登录页面
resp.sendRedirect("/login.html");
}
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
try {
//存到数据库里面
Article article = Article.publish(user,title,body);
if (article != null){
writer.println("<h1>文章发表成功!</h1>");
} else {
writer.println("<h1>文章发表失败!</h1>");
}
} catch (SQLException e){
throw new ServletException(e);
}
}
}
Article.java
package com.peixinchen.model;
import com.peixinchen.util.Database;
import java.sql.*;
import java.util.Date;
public class Article {
public int id;
public String title;
public String body;
public Date publishedAt;
public Article(int id, String title, String body, Date publishedAt) {
this.id = id;
this.title = title;
this.body = body;
this.publishedAt = publishedAt;
}
public static Article publish(User user, String title, String body) throws SQLException {
Date publishedAt = new Date();
String sql = "INSERT INTO articles (author_id,cover_image,title,body,published_at) VALUES(?,'',?,?,?)";
try (Connection con = Database.getConnection()){
try (PreparedStatement stmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)){
stmt.setInt(1,user.id);
stmt.setString(2,title);
stmt.setString(3,body);
stmt.setString(4,User.dateFormat.format(publishedAt));
stmt.executeUpdate();
try (ResultSet rs = stmt.getGeneratedKeys()){
if (!rs.next()){
return null;
}
int id = rs.getInt(1);
Article article = new Article(id,title,body,publishedAt);
return article;
}
}
}
}
}
运行结果
输入内容点击发表:
在数据库可以看到数据已经成功插入了:
这里我找了半天的bug发现是我的JDBC把published写成了pulished,哎一古气死了
<九> 文章管理
- form表单的 attribute 部分
- form表单的 input 部分
- Servlet 的 注解部分
- Servlet 的 处理部分
一、交换数据
writer.jsp
<%@ page import="com.peixinchen.model.User" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
User user =(User) session.getAttribute("user");
if (user == null){
//用户没有登录
response.getWriter().println("<h1>请先登录再进行操作!</h1>");
return;
}
%>
<html>
<head>
<meta charset="UTF-8">
<title>发表文章</title>
</head>
<body>
<h1>发表文章</h1>
<form action="/publish" method="post" enctype="multipart/form-data">
<div>
头图:<input type="file" name="cover_image">
</div>
<div>
标题:<input type="text" name="title">
</div>
<div>
正文:<textarea name="body"></textarea>
</div>
<button type="submit">发表</button>
</form>
</body>
</html>
上传文件
当运行后上传文件,通过抓包工具就可以看到这里数据是已经被成功发送了
说明 form 中通过增加 enctype 和 input 中 type = “file” 已经可以将数据成功发送了,服务器肯定是能收到了,所以下一步要做的就是修改服务器端代码,去接收数据
读取文件
1. @MutiplePartConfig
加了注解之后就像平常读数据那样从请求中读取就ok了
2.加上注解后 通过Part去读
//Part用法及相关属性
Part coverImage = req.getPart("cover_image");
System.out.println(coverImage.getName());
System.out.println(coverImage.getSubmittedFileName());
System.out.println(coverImage.getContentType());
System.out.println(coverImage.getSize());
InputStream inputStream = coverImage.getInputStream();
Scanner scanner = new Scanner(inputStream,"UTF-8");
System.out.println(scanner.next());
运行结果:
上传文件
点击提交 代码打印结果:
总结:
通过form表单上传文件时:
- 客户端 —— 数据可以正确的被发送
1) form表单增加 enctype 部分
2) 添加 input type=file - 服务端 —— 正确的接收发送来的数据
1)Servlet 类上增加 @MutiplePartConfig
2) 代码内部通过 Part 对象读取相应的内容
二、上传文件功能演示
获取绝对路径方法:这个路径是可以自己选择的
代码:
package com.peixinchen.servlets;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.*;
@WebServlet("/publish")
@MultipartConfig
public class PublishServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
Part coverImage = req.getPart("cover_image");
String filename = coverImage.getSubmittedFileName();
String path = "F:\\IDEA\\Servlet-博客\\web\\images\\" + filename;
InputStream is = coverImage.getInputStream();//文件来源
//类似IO时,文件复制的代码,从输入不停的读,将他写到输出里
byte[] buffer = new byte[8192];
int len;
try (OutputStream os = new FileOutputStream(path)) {//最终文件要放的位置
while ((len = is.read(buffer)) != -1 ){
os.write(buffer,0,len);
}
}
}
}
运行结果
1、上传一个 .txt 文件
现在的images下是没有东西的:
我们开始上传文件:
文件:
点击发表:
可以看到文件成功上传到了images目录下:
2、上传一个图片
在浏览器访问刚刚上传的文件
注意: Tomcat访问的是编译后生成的out部分,所以要通过浏览器访问的话,需要把images完整复制到out下面,也可以在上传文件时就将路径选择在这个下面,上传之后就可以直接访问
访问:啊啊啊啊啊伯贤好帅!!对不起跑偏了
三、发表文章
图片保存到本地
- 图片保存下来后,还可以通过tomcat去访问,因为web下放的都是静态资源,可以通过tomcat去访问的,所以就将图片放到 web/images 下
- 保存图片的时候,使用什么文件名称保存
1)使用原有得到文件名:
缺点:如果有恶意用户,准备一个特殊的文件名,造成安全问题 ; 文件如果一样,就会被覆盖
2)自己生成随机的、不重复的文件名
现实中主要使用这种 - 不可以使用相对路径(理论上是可以的,但是需要特殊处理下)
相对路径是相对进程启动时所在的路径而言的
我们运行的所有Servlet代码,都是在Tomcat这个进程内部运行的
相对路径,默认是相对Tomcat的启动路径:Tomcat / bin
发表文章 publishServlet.java
package com.peixinchen.servlets;
import com.peixinchen.model.Article;
import com.peixinchen.model.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.*;
import java.sql.SQLException;
import java.util.Scanner;
@WebServlet("/publish")
@MultipartConfig
public class PublishServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
Part coverImage = req.getPart("cover_image");
String filename = coverImage.getSubmittedFileName();
//下面有两种获取路径的方法
String path = req.getServletContext().getRealPath("images") + "\\" + filename;
//String path = "F:\\IDEA\\Servlet-博客\\out\\artifacts\\Servlet__Web_exploded\\images\\" + filename;
InputStream is = coverImage.getInputStream();//文件来源
//类似IO时,文件复制的代码,从输入不停的读,将他写到输出里
byte[] buffer = new byte[8192];
int len;
try (OutputStream os = new FileOutputStream(path)) {//最终文件要放的位置
while ((len = is.read(buffer)) != -1 ){
os.write(buffer,0,len);
}
}
req.setCharacterEncoding("utf-8");
String title = req.getParameter("title");
String body = req.getParameter("body");
String coverImageUrl = "/images/" + filename;
HttpSession session = req.getSession();
User user =(User) session.getAttribute("user");
if (user == null){
//说明没有登录,跳转到登录页面
resp.sendRedirect("/login.html");
}
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
try {
//存到数据库里面
Article article = Article.publish(user,title,body,coverImageUrl);
if (article != null){
writer.println("<h1>文章发表成功!</h1>");
} else {
writer.println("<h1>文章发表失败!</h1>");
}
} catch (SQLException e){
throw new ServletException(e);
}
}
}
Articles.java _ publish()
public static Article publish(User user, String title, String body , String coverImageUrl) throws SQLException {
Date publishedAt = new Date();
String sql = "INSERT INTO articles (author_id,cover_image,title,body,published_at) VALUES(?,?,?,?,?)";
try (Connection con = Database.getConnection()){
try (PreparedStatement stmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)){
stmt.setInt(1,user.id);
stmt.setString(2,coverImageUrl);
stmt.setString(3,title);
stmt.setString(4,body);
stmt.setString(5,User.dateFormat.format(publishedAt));
stmt.executeUpdate();
try (ResultSet rs = stmt.getGeneratedKeys()){
if (!rs.next()){
return null;
}
int id = rs.getInt(1);
return new Article(id,title,body,publishedAt);
}
}
}
}
运行结果
登录后,进行发表文章:
查看数据库可以看到已经成功发布了:
可以看到头图cover_image的信息,将信息复制拿到浏览器访问,可以看到成功拿到了头图:
四 、 文章详情页
Article.java中成员加上:
public String coverImage;
a.jsp
<%@ page import="com.peixinchen.model.Article" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
int id = Integer.parseInt(request.getParameter("id"));
//TODO : 合法性校验
Article article = Article.getById(id);
if (article == null){
response.setStatus(404);
return;
}
%>
<html>
<head>
<meta charset="UTF-8">
<title><%= article.title %>></title>
</head>
<body>
<img src="<%= article.coverImage %>">
<h1><%= article.title %></h1>
<p><%= article.body %></p>
<!-- TODO : 作者、文章发表时间 都可以写 这里没有写 -->
</body>
</html>
Article.java_getById()
public static Article getById(int id) throws SQLException {
String sql = "SELECT cover_image,title,body,published_at FROM articles WHERE id = ?";
try (Connection con = Database.getConnection()){
try (PreparedStatement stmt = con.prepareStatement(sql)) {
stmt.setInt(1,id);
try (ResultSet rs = stmt.executeQuery()){
if (!rs.next()) {
return null;
}
String coverImage = rs.getString(1);
String title = rs.getString(2);
String body = rs.getString(3);
Date publishedAt = User.dateFormat.parse(rs.getString(4));
Article article = new Article(id,title,body,publishedAt);
article.coverImage = coverImage;//这其实就是构造方法,我没有改构造方法是因为,改了之后前面很多代码都无法运行了
return article;
} catch (ParseException e){
return null;
}
}
}
运行结果
五、文章列表
JSON格式返回
JSON的jar包
把jar包拷到lib目录下,依赖加进去
libraries加进去了之后别忘了Artifacts:
https://search.maven.org/
可以在官网下载想要的jar包
这个是我用的:
本地方法实现
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
public class Main {
public static void main(String[] args) {
JSONArray array = new JSONArray();
//当成key-value去使用就好了
JSONObject o1 = new JSONObject();
o1.put("id",1);
o1.put("cover_image","images/1.png");
o1.put("title","title1");
o1.put("body","body1");
array.add(o1);//到这里就有一个对象了
//同理,可以有很多个对象
JSONObject o2 = new JSONObject();
o2.put("id",2);
o2.put("cover_image","images/2.png");
o2.put("title","title2");
o2.put("body","body2");
array.add(o2);
System.out.println(array.toJSONString());
}
}
运行结果:
Servlet实现 ListServlet.java && Article.java_list()
package com.peixinchen.servlets;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.peixinchen.model.Article;
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.io.PrintWriter;
import java.sql.SQLException;
import java.util.List;
@WebServlet("/list.json")
public class ListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
try {
List<Article> articleList = Article.list();
String jsonString = transform(articleList);
writer.println(jsonString);
} catch (SQLException e){
throw new ServletException(e);
}
}
private String transform(List<Article> articleList) {
JSONArray array = new JSONArray();
for (Article article : articleList){
JSONObject o = new JSONObject();
o.put("id",article.id);
o.put("cover_image",article.coverImage);
o.put("title",article.title);
o.put("body",article.body);
array.add(o);
}
return array.toJSONString();
}
}
Article.java_list()
public static List<Article> list() throws SQLException{
String sql = "SELECT id,cover_image,title,body,published_at FROM articles ORDER BY id DESC";
List<Article> articleList = new ArrayList<>();
try (Connection con = Database.getConnection()){
try (PreparedStatement stmt = con.prepareStatement(sql)) {
try (ResultSet rs = stmt.executeQuery()){
while (rs.next()){
int id = rs.getInt(1);
String coverImage = rs.getString(2);
String title = rs.getString(3);
String body = rs.getString(4);
try {
Date publishedAt = User.dateFormat.parse(rs.getString(5));
Article article = new Article(id,title,body,publishedAt);
article.coverImage = coverImage;
articleList.add(article);
} catch (ParseException e){
continue;
}
}
}
}
}
return articleList;
}
运行结果:
如何保证Servlet项目中的字符集正确
<十> 用户注销
删掉Cookie其实就相当于用户注销了