引入
效果图
Eclipse EE上集成tomcat
1、添加一个Server
2、 添加servlet相关的jar包
3、设置端口号和context-path
Dao层封装jdbc,简化数据库操作
1、导入对应版本的驱动jar包
2、编写数据库连接的自定义配置文件
3、编写获取数据库连接的类
4、编写基础的数据库操作工具类
5、编写数据库表对应的实体类
编写Servlet
1、生成验证码图片的servlet
2、登录的servlet
3、退出登录的servlet
编写Service层
编写登录过滤器
其实这源于软件体系结构这门课程的实验作业,体验B/S体系结构风格,原实验要求我们实现简单的注册登录功能。然而学校的课程体系结构很迷,php和java web等技术型课程全是选修,而且这学期的java web课程,跳过了servlet直接教SSM。
所以实验课老师给出了php和java的代码,但是代码比较简单,不考虑功能扩展。我还是选择自己写吧。。。
在Eclipse EE上集成tomcat,自己封装原生 servlet 和 jdbc,实现登录demo。同时,这样的封装能够支持在不用任何java 框架的情况下,实现一个小型web项目。
tomcat是支持servlet和jsp的、用java写的一个web应用服务器。servlet和jsp相关的jar包,并不是jdk自带,而是来源于tomcat的类库(安装目录下的lib目录)。我们需要为新建的web Project添加tomcat的运行类库,否则在project中没办法使用servlet和jsp。
JDBC.properties
# 中心仓库:https://mvnrepository.com/
# mysql驱动jar包
# 8.0.16:https://mvnrepository.com/artifact/mysql/mysql-connector-java/8.0.16
# 5.1.47:https://mvnrepository.com/artifact/mysql/mysql-connector-java/5.1.47
# 数据库连接的基本配置
user=root
password=ysqJYKL2010
# 不设置useSSL会有警告
url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=true
driver=com.mysql.jdbc.Driver
DbConnector.java
package dao;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import utils.CloseUtil;
/**
* 数据库连接类 初始化数据库连接配置,获取数据库连接
*
* @author passerbyYSQ
* @date 2020-9-29 14:19:37
*/
public class DbConnector {
/**
* 连接数据库的基本配置,从配置文件读入。
*/
private static String user;
private static String password;
// 数据库连接协议的url
private static String url;
// 数据库驱动包的类路径
private static String driver;
// 存储配置文件的数据结构Properties
private static Properties configProp = new Properties();
static {
InputStream inputStream = null;
try {
ClassLoader classLoader = DbConnector.class.getClassLoader();
// 在src目录下获取JDBC.properties的输入流
inputStream = classLoader.getResourceAsStream("JDBC.properties");
if (inputStream == null) {
throw new RuntimeException("配置文件不存在");
}
// 将配置文件加载到configProp
configProp.load(inputStream);
user = configProp.getProperty("user");
password = configProp.getProperty("password");
url = configProp.getProperty("url");
driver = configProp.getProperty("driver");
// 注册数据库驱动包
Class.forName(driver);
// System.out.println("注册数据库驱动包成功!");
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
CloseUtil.close(inputStream);
}
}
/**
* 获取一个数据库连接对象
*
* @return
*/
public static Connection getConnection() {
try {
return DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
/**
* 释放资源
*
* @param stmt 执行sql语句的对象
* @param conn 数据库连接对象
*/
public static void close(Statement stmt, Connection conn) {
close(null, stmt, conn);
}
/**
* 释放资源的重载形式
*
* @param rs 结果集对象
* @param stmt 执行sql语句的对象
* @param conn 数据库连接对象
*/
public static void close(ResultSet rs, Statement stmt, Connection conn) {
try {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
DaoUtil.java
package dao;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 基础的数据库操作工具类
* 简化通用的基础的增删改查的操作,与具体业务逻辑无关
*
* @author passerbyYSQ
* @date 2020-9-29 14:51:16
*/
public class DaoUtil {
/**
* 通用的查询操作
*
* @param 查询返回的数据,基类中无法确定,需要通过泛型抛给子类
* @param sql sql语句(里面有占位符)
* @param pack 一个接口,该接口有个抽象方法将ResultSet回调给子类,让子类来处理ResultSet中数据
* @param args 与占位符一一对应的参数值
* @return 封装后的查询结果
*/
public static T select(String sql, PackResult pack, Object... args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet resultSet = null;
T result = null;
try {
conn = DbConnector.getConnection();
pstmt = conn.prepareStatement(sql);
// 将参数设置到sql语句的占位符中
setValue(pstmt, args);
resultSet = pstmt.executeQuery();
// 由于不同的表对应不同的实体类,在基类中无法实现将resultSet封装成对应的实体类返回
// 故只能通过接口中的回调函数交给子类去实现
result = pack.onResultReturn(resultSet);
return result;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
DbConnector.close(resultSet, pstmt, conn);
}
}
/**
* 获取结果集数量
* @param from from部分,传递时不需要带"from"。之所以传递from部分,而不是表名,是考虑到多表查询
* @param where where部分,传递时不需要带"where",里面含有占位符
* @param args 代替占位符的参数值
* @return 记录数
*/
public static int getCount(String from, String where, Object... args) {
String sql = "select count(*) from " + from + " where " + where;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet resultSet = null;
try {
conn = DbConnector.getConnection();
pstmt = conn.prepareStatement(sql);
// 将参数设置到sql语句的占位符中
setValue(pstmt, args);
resultSet = pstmt.executeQuery();
// 获取失败返回-1
return resultSet.next() ? resultSet.getInt(1) : -1;
} catch (Exception e) {
e.printStackTrace();
return -1;
} finally {
DbConnector.close(resultSet, pstmt, conn);
}
}
/**
* 插入记录,比较适用于插入一条或几条数据。不适合插入一个集合的数据
* @param sql sql语句(里面有占位符)
* @param args 与占位符一一对应的参数值
* @return 受影响的行数
*/
public static int insertOrUpdateOrDelete(String sql, Object... args) {
Connection conn = null;
PreparedStatement pstmt = null;
int count = 0;
try {
conn = DbConnector.getConnection();
pstmt = conn.prepareStatement(sql);
// 将参数设置到sql语句的占位符中
setValue(pstmt, args);
//System.out.println(pstmt.toString());
// 受影响的行数
count = pstmt.executeUpdate();
return count;
} catch (Exception e) {
e.printStackTrace();
return -1;
} finally {
DbConnector.close(pstmt, conn);
}
}
/**
* 事务操作
* 如果实现类不使用回调的PreparedStatement和ResultSet的引用,
* 而是选择自己new新的实例,那么就需要自己负责资源关闭
*
* @param sqlEntitys
*/
public static void updateInTransaction(MoreUpdate moreUpdate) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = DbConnector.getConnection();
conn.setAutoCommit(false); // 开启事务(手动提交)
moreUpdate.updateActions(conn, pstmt, rs);
conn.commit(); // 提交事务
} catch (SQLException e) {
e.printStackTrace();
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
DbConnector.close(rs, pstmt, conn);
}
}
/**
* 插入一条记录。如果主键是自增长的,返回该条记录的id
* 注意:主键必须是自增长的!!!
* @param sql
* @param args
* @return
*/
public static int insertReturnId(String sql, Object... args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = DbConnector.getConnection();
pstmt = conn.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
// 将参数设置到sql语句的占位符中
setValue(pstmt, args);
//System.out.println(pstmt.toString());
pstmt.executeUpdate();
// 获取最后插入的记录的自增长id。修改是不会返回的
rs = pstmt.getGeneratedKeys();
return rs.next() ? rs.getInt(1) : 0;
} catch (Exception e) {
e.printStackTrace();
return -1;
} finally {
DbConnector.close(rs, pstmt, conn);
}
}
// 将参数设置到sql语句的占位符中
public static void setValue(PreparedStatement pstmt, Object... args) throws SQLException {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String) { // 主要
pstmt.setString(i + 1, (String) args[i]);
} else if (args[i] instanceof Integer) { // 主要
int num = (Integer) args[i];
pstmt.setInt(i + 1, num);
} else if (args[i] instanceof LocalDateTime) { // 主要
pstmt.setTimestamp(i + 1, Timestamp.valueOf((LocalDateTime) args[i]));
} else if (args[i] instanceof Date) { // 主要 ((Date) args[i]).getTime()
pstmt.setTimestamp(i + 1, new Timestamp(((Date) args[i]).getTime()));
} else if (args[i] instanceof BigDecimal) {
pstmt.setBigDecimal(i + 1, (BigDecimal) args[i]);
} else if (args[i] instanceof Double) {
pstmt.setDouble(i + 1, (Double) args[i]);
} else if (args[i] instanceof Float) {
pstmt.setFloat(i + 1, (Float) args[i]);
} else if (args[i] instanceof Boolean) {
pstmt.setBoolean(i + 1, (boolean) args[i]);
} else {
throw new RuntimeException("DaoUtil类暂不支持set该数据类型");
}
}
}
public interface PackResult {
// 将ResultSet回调给子类,让调用者来处理ResultSet中的数据(转换成特定的pojo)
T onResultReturn(ResultSet rs) throws Exception;
}
public interface MoreUpdate {
/**
* 多种update相关的操作
*
* 如果实现类不使用回调的pstmt,而是选择自己new新的pstmt,那么
* 就需要自己负责pstmt的关闭操作。
*
* @param conn
* @param pstmt 回调给实现类的是null
* @throws SQLException
*/
void updateActions(Connection conn, PreparedStatement pstmt, ResultSet rs) throws SQLException;
}
}
这里只有一张 tb_user 表,对应 User 实体类
User.java
package entity;
import java.time.LocalDateTime;
/**
* 用户实体类
* 数据库表所对应的实体类
*
* @author passerbyYSQ
* @date 2020-9-30 20:46:28
*/
public class User {
private Integer userId;
private String username;
private String password;
// 最后一次登录的时间
private LocalDateTime lastLoginTime;
public User(Integer userId, String username) {
this.userId = userId;
this.username = username;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public LocalDateTime getLastLoginTime() {
return lastLoginTime;
}
public void setLastLoginTime(LocalDateTime lastLoginTime) {
this.lastLoginTime = lastLoginTime;
}
}
VerificationCodeServlet.java
package servlet;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 生成简单的验证码图片
*
* @author passerbyYSQ
* @date 2020-9-30 20:24:02
*/
@WebServlet("/verification_code")
public class VerificationCodeServlet extends HttpServlet {
private static final long serialVersionUID = 2523187418823246381L;
private Integer width = 100;
private Integer height = 40;
private Integer charCnt = 4; // 验证码图片中字符的个数
private Integer lineCnt = 8;
private Random r = new Random(); // 用于生产随机数
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 创建图片对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// 填充背景色
g.setColor(getBgColor());
g.fillRect(0, 0, width, height);
g.setFont(new Font("黑体", Font.BOLD, 30));
// 写字符
StringBuilder sbd = new StringBuilder();
for (int i = 1; i <= charCnt; i++) {
g.setColor(getCharColor());
int x = width / (charCnt + 2) * i;
int y = height * 2 / 3;
char c = getRandomChar();
sbd.append(c);
g.drawString(c + "", x, y);
}
request.getSession().setAttribute("verification_code", sbd.toString());
for (int i = 1; i < lineCnt; i++) {
int x1 = r.nextInt(width);
int y1 = r.nextInt(height);
int x2 = r.nextInt(width);
int y2 = r.nextInt(height);
g.setColor(getLineColor());
g.drawLine(x1, y1, x2, y2);
}
// 输出图片
ImageIO.write(image, "jpg", response.getOutputStream());
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doPost(request, response);
}
/**
* 生成一个在区间[low, high]的随机整数
*
* @param low
* @param high
* @return
*/
private int getInt(int low, int high) {
return (low + r.nextInt(high - low + 1));
}
/**
* 生成一种背景色(相对字符颜色来说较浅)
*
* @return
*/
private Color getBgColor() {
int red = getInt(170, 255);
int green = getInt(170, 255);
int blue = getInt(170, 255);
return new Color(red, green, blue);
}
/**
* 生成一种干扰线的颜色
*
* @return
*/
private Color getLineColor() {
int red = getInt(85, 169);
int green = getInt(85, 169);
int blue = getInt(85, 169);
return new Color(red, green, blue);
}
/**
* 生成一种字符颜色(相对字体颜色较深)
*
* @return
*/
private Color getCharColor() {
int red = getInt(0, 84);
int green = getInt(0, 84);
int blue = getInt(0, 84);
return new Color(red, green, blue);
}
/**
* 生成一个随机字符
*
* @return
*/
private char getRandomChar() {
final String chs = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
return chs.charAt(r.nextInt(chs.length()));
}
}
LoginServlet.java
package servlet;
import java.io.IOException;
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 entity.User;
import service.AccountService;
import service.impl.AccountServiceImpl;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1821754212240924673L;
private AccountService accountService = new AccountServiceImpl();
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取username
String username = request.getParameter("username");
// 获取密码
String password = request.getParameter("password");
// 简单的参数判断
if (username == null || "".equals(username)) {
request.setAttribute("login_error", "用户名不能为空");
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}
if (password == null || "".equals(password)) {
request.setAttribute("login_error", "密码不能为空");
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}
// 获取用户输入的验证码
String codeByUser = request.getParameter("verification_code");
HttpSession session = request.getSession();
// 取出正确的验证码
String correctCode = (String) session.getAttribute("verification_code");
// 验证码是一次性的,取出后失效
session.removeAttribute("verification_code");
if (codeByUser == null || "".equals(codeByUser)
|| !codeByUser.equalsIgnoreCase(correctCode)) {
request.setAttribute("login_error", "验证码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}
User user = accountService.login(username, password);
if (user == null) {
request.setAttribute("login_error", "用户名或者密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
// 登录成功
session.setAttribute("username", user.getUsername());
response.sendRedirect(request.getContextPath() + "/index.jsp");
}
}
LogoutServlet.java
package servlet;
import java.io.IOException;
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;
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
private static final long serialVersionUID = 5967081510030254471L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
String username = (String) session.getAttribute("username");
if (username != null) {
session.removeAttribute("username");
}
response.sendRedirect(request.getContextPath() + "/login.jsp");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
AccountServiceImpl.java
package service.impl;
import java.sql.ResultSet;
import java.time.LocalDateTime;
import dao.DaoUtil;
import entity.User;
import service.AccountService;
/**
* @author passerbyYSQ
* @date 2020-9-30 20:50:33
*/
public class AccountServiceImpl implements AccountService {
@Override
public User login(String username, String password) {
String sql = "select * from tb_user where username=? and password=?";
User user = DaoUtil.select(sql, new DaoUtil.PackResult() {
@Override
public User onResultReturn(ResultSet rs) throws Exception {
if (!rs.next()) {
return null;
}
int userId = rs.getInt("id");
String username = rs.getString("username");
// 密码不需要取出
return new User(userId, username);
}
}, username, password);
// 登录成功,更新最后一次登录的时间
if (user != null) {
sql = "update tb_user set last_login_time=? where id=?";
DaoUtil.insertOrUpdateOrDelete(sql, LocalDateTime.now(), user.getUserId());
}
return user;
}
}
非登录用户不能访问主页,已登录用户不能访问登录页面
LoginFilter.java
package filter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 拦截器,用于验证是否已经登录
*
* @author passerbyYSQ
* @date 2020-10-1 16:28:20
*/
@WebFilter(
filterName = "loginFilter",
// 实际上可以把需要拦截的页面分类到某个目录下,某些api也分类,
// 这样拦截的针对性更强,需要排除的也更少
urlPatterns = "/*", // 拦截所有url
initParams = {
// 不拦截某些页面、api、静态资源
@WebInitParam(name = LoginFilter.EXECUDED_PAGE, value="login.jsp"),
@WebInitParam(name = LoginFilter.EXECUDED_API,
value="login;logout;verification_code"),
@WebInitParam(name = LoginFilter.EXECUDED_RESOURCE,
value=".jpg;.png;.jpeg;.css;.js"),
@WebInitParam(name = "encoding", value = "utf-8"), // 编码
}
)
public class LoginFilter implements Filter {
// 页面
public static final String EXECUDED_PAGE = "EXECUDED_PAGE";
// API
public static final String EXECUDED_API = "EXECUDED_API";
// 静态资源
public static final String EXECUDED_RESOURCE = "EXECUDED_RESOURCE";
private FilterConfig filterConfig;
private Set execudedUrl = new HashSet<>();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
processInitParamArray(EXECUDED_PAGE);
processInitParamArray(EXECUDED_API);
processInitParamArray(EXECUDED_RESOURCE);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 设置字符集,防止post表单乱码
String encoding = filterConfig.getInitParameter("encoding");
if (encoding == null) {
encoding = "utf-8";
}
httpRequest.setCharacterEncoding(encoding);
// 判断是否需要拦截。默认需要拦截
boolean isNeedFilter = true;
String uri = httpRequest.getRequestURI();
for (String str : execudedUrl) {
if (uri.endsWith(str)) { // 注意用endsWith
// 不需要拦截
isNeedFilter = false;
break;
}
}
HttpSession session = httpRequest.getSession();
String username = (String) session.getAttribute("username");
// 不需要拦截,放行
if (!isNeedFilter) {
if ((uri.endsWith("login") || uri.endsWith("login.jsp")) && username != null) {
// 已登录,不能访问登录页面
httpResponse.sendRedirect(httpRequest.getContextPath() + "/index.jsp");
} else {
chain.doFilter(httpRequest, httpResponse);
}
} else {
if (username != null) {
// 已登录,放行
chain.doFilter(httpRequest, httpResponse);
} else {
// 未登录。重定向到登录界面
httpResponse.sendRedirect(httpRequest.getContextPath() + "/login.jsp");
}
}
}
@Override
public void destroy() {
filterConfig = null;
execudedUrl.clear();
}
private void processInitParamArray(String key) {
String param = filterConfig.getInitParameter(key);
if (param != null) {
String[] split = param.split(";");
for (String str : split) {
// 静态资源
if (!"".equals(str)) {
execudedUrl.add(str);
}
}
}
}
}
好了,完结撒花!!!
前端页面比较简单,我就不贴出来了。有的人可能会问,不就是一个登录功能,用得着写这么多吗?注意,我们的着眼点是:封装原生的servlet和jdbc,能够扩展支持小型的web项目开发。只不过我们以登录功能作为demo而已。这对于学习过java web但又没接触过java框架的同学,有比较大的借鉴意义。