最近开始学习了Javaweb,东西有点多,先来梳理一下相关的知识
- 什么是Servlet?
- Tomcat的运行过程
- 什么是ServletConfig?
- 什么是ServletContext?
- Servlet的生命周期
- 注解的使用
- 模板方法的设计模式
- request 请求对象......
什么是Servlet
Servlet其实是一个接口,任何实现了Servlet接口的类,都被称为Servlet类
而所有的servlet类都由服务器调度,创建和销毁,无需我们程序员干预,要注意,我们自己定义的Servlet类,也就是我们自己new出来,而不是由服务器创建的servlet类,不受服务器管理
服务器有很多,我这里使用的是Tomcat服务器,我们可以把服务器理解为一个容器,它存储着很多应用,而应用是由Servlet类组合形成的
服务器和浏览器遵循着Http协议
Servlet类(实现类)和数据库遵循着JDBC协议
而服务器和Servlet类遵循着Servlet规范,也就是说,Servlet规范是服务器与实现类之间的协议
上图:
我们只需要记住,实现了Servlet接口的类,被称为Servlet类
而实现了Servlet的类,就是项目的功能实现
Tomcat的运行过程
我这里画了一个图,展示了浏览器,服务器,Servlet与数据库的实现过程
首先用户在浏览器客户端输出网址,服务器也就是tomcat获取网址,通过tcp协议获取端口,项目名,和跳转连接,来访问对应的项目,比如我输入的是taobao,那么就会访问taobao的项目,如果输入的是xiaohongshu,那么就会访问xiaohongshu的项目,找到对应的项目后,会首先访问index.html(服务器的配置文件),当我们点击登录,访问login.html,实际的登录操作,是由登录这个Servlet类实现的,该类就需要拿到用户输入的数据,去数据库进行操作,最后将响应返回到浏览器客户端上。
什么是ServletConfig
在Tomcat中我们需要实现一个配置文件,web.xml文件,里面存放着许多配置信息
用户输入链接是怎么执行相应的Servlet类呢,就是通过web.xml文件
用户输入....../dept/list该链接,就会获取其链接的名字,通过该名字去找其名字对应的class文件,通过反射执行Servlet类,所以,链接的命名和class文件的命名必须一样
被servlet和servlet-mapping包裹的信息,注意是一对,被称为一个ServletConfig对象,一个Servlet类对应一个ServletConfig对象。
什么是ServletContext
一个Servlet对象对应一个ServletConfig。100个Servlet对象则对应100个ServletConfig对象。只要在同一个webapp当中,只要在同一个应用当中,所有的Servlet对象都是共享同一ServletContext对象
ServletContext被称为Servlet上下文对象
一个ServletContext对象通常对应的是一个web.xml文件
Servlet的生命周期
实现Servlet接口有几个重要的方法:
Servlet对象像一个人的一生:
Servlet的无参数构造方法执行:标志着你出生了。
Servlet对象的init方法的执行:标志着你正在接受教育。
Servlet对象的service方法的执行:标志着你已经开始工作了,已经开始为人类提供服务了。
Servlet对象的destroy方法的执行:标志着临终。有什么遗言,抓紧的。要不然,来不及了。
Servlet类中方法的调用次数?
构造方法只执行一次。
init方法只执行一次。
service方法:用户发送一次请求则执行一次,发送N次请求则执行N次。
destroy方法只执行一次。
这个文章描述的很详细!
Servlet 的生命周期详解_servlet的生命周期-CSDN博客
例子
下面我写一个简单的例子,在页面进行增删改查的操作
web.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>list</servlet-name>
<servlet-class>com.servlet.DepListServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>list</servlet-name>
<url-pattern>/dept/list</url-pattern>
</servlet-mapping>
</web-app>
这里我简单的配置页面信息,作为页面的展示页,为一个servlet起名为list,当用户访问/dept/list时候就会执行对应的servlet
DepListServlet类
package com.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import oa.tool.ConnectionGet;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DepListServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//获取应用的根路径
String contextPath = request.getContextPath();
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print("<!DOCTYPE html>");
out.print("<html>");
out.print("<head>");
out.print("<meta charset='utf-8'>");
out.print("<title>部门列表页面</title>");
out.print("</head>");
out.print("<body>");
out.print("<script type='text/javascript'>");
out.print("function del(dno){");
out.print("if(window.confirm('删除将不可恢复')){");
out.print("document.location.href = '" + contextPath +"/dept/delete?deptho=' + dno");
out.print("}");
out.print("}");
out.print("</script>");
out.print("<h1 align='center'>部门列表</h1>");
out.print("<hr>");
out.print("<table border='1px' align='center' width='50%'>");
out.print(" <tr>");
out.print(" <th>序号</th>");
out.print(" <th>部门编号</th>");
out.print(" <th>部门名称</th>");
out.print(" <th>部门位置</th>");
out.print(" <th>操作</th>");
out.print(" </tr>");
try {
Connection connection = ConnectionGet.getConnection();
String sql = "select * from dept;";
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet resultSet = ps.executeQuery();
int i = 0;
while (resultSet.next()) {
String deptno = resultSet.getString("deptno");
String dname = resultSet.getString("dname");
String loc = resultSet.getString("loc");
out.print(" <tr>");
out.print(" <td>" + (++i) + "</td>");
out.print(" <td>" + deptno + "</td>");
out.print(" <td>" + dname + "</td>");
out.print(" <td>" + loc + "</td>");
out.print(" <td>");
out.print(" <a href='javascript:void(0)' onclick='del(" + deptno + ")'>删除</a>");
out.print(" <a href='" + contextPath + "/dept/modify?deptno=" + deptno + "'>修改</a>");
out.print(" <a href='" + contextPath + "/dept/details?deptno=" + deptno + "'>详情</a>");
out.print(" </td>");
out.print(" </tr>");
}
ConnectionGet.close(connection, ps, resultSet);
} catch (SQLException e) {
throw new RuntimeException(e);
}
out.print("</table>");
out.print("<hr>");
out.print("<a href='"+contextPath+"/add.html'>新增部门</a>");
out.print("</body>");
out.print("</html>");
}
}
这是一个页面展示的servlet,首先规定了输出,转化为HTML语言,try块里才是这个servlet的主要逻辑,其实就是对JDBC的使用,连接数据库并输出到页面上。
删除,修改,详情也是同理,都是对JDBC的使用,我就不去展示了
我们下面还需要写,删除servlet,修改servlet,详情 servlet,新增servlet,我们的xml文件会很多很多,不能写一个servlet就要配置一个吧,完成后这个文件可能几m了,而且重复工作很费力,这时我们需要一个功能,注解式开发
注解的使用
注解的使用将不再在web.xml中配置servlet,我们通过注解的方式,来指定每个servlet对应的url,如下所示
package com.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import oa.tool.ConnectionGet;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@WebServlet("/dept/list")
public class DepListServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//获取应用的根路径
String contextPath = request.getContextPath();
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print("<!DOCTYPE html>");
out.print("<html>");
out.print("<head>");
out.print("<meta charset='utf-8'>");
out.print("<title>部门列表页面</title>");
out.print("</head>");
out.print("<body>");
out.print("<script type='text/javascript'>");
out.print("function del(dno){");
out.print("if(window.confirm('删除将不可恢复')){");
out.print("document.location.href = '" + contextPath +"/dept/delete?deptho=' + dno");
out.print("}");
out.print("}");
out.print("</script>");
out.print("<h1 align='center'>部门列表</h1>");
out.print("<hr>");
out.print("<table border='1px' align='center' width='50%'>");
out.print(" <tr>");
out.print(" <th>序号</th>");
out.print(" <th>部门编号</th>");
out.print(" <th>部门名称</th>");
out.print(" <th>部门位置</th>");
out.print(" <th>操作</th>");
out.print(" </tr>");
try {
Connection connection = ConnectionGet.getConnection();
String sql = "select * from dept;";
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet resultSet = ps.executeQuery();
int i = 0;
while (resultSet.next()) {
String deptno = resultSet.getString("deptno");
String dname = resultSet.getString("dname");
String loc = resultSet.getString("loc");
out.print(" <tr>");
out.print(" <td>" + (++i) + "</td>");
out.print(" <td>" + deptno + "</td>");
out.print(" <td>" + dname + "</td>");
out.print(" <td>" + loc + "</td>");
out.print(" <td>");
out.print(" <a href='javascript:void(0)' onclick='del(" + deptno + ")'>删除</a>");
out.print(" <a href='" + contextPath + "/dept/modify?deptno=" + deptno + "'>修改</a>");
out.print(" <a href='" + contextPath + "/dept/details?deptno=" + deptno + "'>详情</a>");
out.print(" </td>");
out.print(" </tr>");
}
ConnectionGet.close(connection, ps, resultSet);
} catch (SQLException e) {
throw new RuntimeException(e);
}
out.print("</table>");
out.print("<hr>");
out.print("<a href='"+contextPath+"/add.html'>新增部门</a>");
out.print("</body>");
out.print("</html>");
}
}
没有任何变化,只是删除了配置文件的配置信息,然后在类上添加@WebServlet("/dept/list"),也就是配置文件中url-pattern对应的路径。
模板设计模式
我们看到我们这个包下,CRUD的操作就需要这么多类,以后如果有需要是不是要好多好多类,出现类爆炸的问题,所以我们能不能将这些servlet归到一个类中,统一进行管理呢?
还记得我们之前写的学生管理系统的主页面吗,如果输入是1,就展示,如果输入的是2,就添加,一个数字对应一个方法,其实这就是模板设计模式,提取出主干,在主干上添加方法
我们将对应的url统一添加到整体的servlet上,通过用户输入的地址,来找到输入地址的对应方法。
重定向与转发
在一个web应用中通过两种方式,可以完成资源的跳转:
第一种方式:转发
第二种方式:重定向
转发和重定向有什么区别?
代码上有什么区别?
转发:
// 获取请求转发器对象
RequestDispatcher dispatcher = request.getRequestDispatcher("/dept/list");
// 调用请求转发器对象的forward方法完成转发
dispatcher.forward(request, response);
// 合并一行代码
request.getRequestDispatcher("/dept/list").forward(request, response);
// 转发的时候是一次请求,不管你转发了多少次。都是一次请求。
// AServlet转发到BServlet,再转发到CServlet,再转发到DServlet,不管转发了多少次,都在同一个request当中。
// 这是因为调用forward方法的时候,会将当前的request和response对象传递给下一个Servlet。
重定向:
// 浏览器发送请求,请求路径上是需要添加项目名的。
// 以下这一行代码会将请求路径“/oa/dept/list”发送给浏览器
// 浏览器会自发的向服务器发送一次全新的请求:/oa/dept/list
response.sendRedirect("/oa/dept/list");
转发(一次请求):
在浏览器地址栏上发送的请求是:http://localhost:8080/servlet/a ,最终请求结束之后,浏览器地址栏上的地址还是这个。没变。
重定向(两次请求):
在浏览器地址栏上发送的请求是:http://localhost:8080/servlet/a ,最终在浏览器地址栏上显示的地址是:http://localhost:8080/servlet/b
转发和重定向的本质区别?
转发:是由WEB服务器来控制的。A资源跳转到B资源,这个跳转动作是Tomcat服务器内部完成的。
重定向:是浏览器完成的。具体跳转到哪个资源,是浏览器说了算。
我们可以这样理解:
转发是在服务器内部做的事情,浏览器只关心最后响应的结果,也就是说用户访问a的servlet,a的servlet结束后会再去执行到b的servlet,最后响应的是b的servlet,但是浏览器显示的是a的servlet
而重定向就是访问两次不同的服务器,发送了两次请求,每次发送请求给指定服务器的时候都是使用的是不同的Request请求对象和Response响应对象,所以整个请求过程中,数据是不共享的
转发和重定向应该如何选择?什么时候使用转发,什么时候使用重定向?
如果在上一个Servlet当中向request域当中绑定了数据,希望从下一个Servlet当中把request域里面的数据取出来,使用转发机制。
剩下所有的请求均使用重定向。(重定向使用较多。)(因为还有Session)
域
在详细说Cookie和Session之前,我们来说一下我们目前遇到的作用域
request(对应的类名:HttpServletRequest) 请求域(请求级别的)
session(对应的类名:HttpSession) 会话域(用户级别的)
application(对应的类名:ServletContext) 应用域(项目级别的,所有用户共享的。)
这三个域对象的大小关系
request < session < application
他们三个域对象都有以下三个公共的方法:
setAttribute(向域当中绑定数据)
getAttribute(从域当中获取数据)
removeAttribute(删除域当中的数据)
Seesion
什么是session?
用户打开浏览器,进行一系列操作,然后最终将浏览器关闭,这个整个过程叫做:一次会话。会话在服务器端也有一个对应的java对象,这个java对象叫做:session。
什么是一次请求:用户在浏览器上点击了一下,然后到页面停下来,可以粗略认为是一次请求。请求对应的服务器端的java对象是:request。
session对象最主要的作用是:保存会话状态。(用户登录成功了,这是一种登录成功的状态,怎么把登录成功的状态一直保存下来呢?使用session对象可以保留会话状态。)
为什么需要session对象来保存会话状态呢?
因为HTTP协议是一种无状态协议。
什么是无状态:请求的时候,B和S是连接的,但是请求结束之后,连接就断了。为什么要这么做?HTTP协议为什么要设计成这样?因为这样的无状态协议,可以降低服务器的压力。请求的瞬间是连接的,请求结束之后,连接断开,这样服务器压力小。
只要B和S断开了,那么关闭浏览器这个动作,服务器知道吗?
不知道。服务器是不知道浏览器关闭的。
我们可以将session看成是一个浏览器对服务器访问,该浏览器对服务器的所有操作都称为一次会话(session)
session的实现原理:
当我们第一次访问服务器时,服务器会生成一个session对象,并给该session对象生成一个id,然后web服务器会将session的id发送给浏览器,浏览器将session的id保存在浏览器的缓存中。
session列表是一个Map,map的key是session-id,map的value是session对象。
用户第二次请求,自动将浏览器内存中的id发送给服务器,服务器根据id查找session对象。
关闭浏览器,内存消失,cookie消失,sessionid消失,会话等同于结束。
session的销毁:
浏览器关闭的时候,服务器是不知道的,服务器无法监session对象什么时候被销毁?测到浏览器关闭了,所以session的销毁要依靠session超时机制。但也有一种可能,系统提供了“安全退,用户可以点击这个按钮,这样服务器就知道你退出了,然后服务器会自动销毁session对象
第一种销毁:是超时销毁
第二种销毁:是手动销毁
Cookie
session的实现原理中,每一个session对象都会关联一个sessionid,例如:
JSESSIONID=41C481F0224664BDB28E95081D23D5B8
以上的这个键值对数据其实就是cookie对象。
对于session关联的cookie来说,这个cookie是被保存在浏览器的“运行内存”当中。
只要浏览器不关闭,用户再次发送请求的时候,会自动将运行内存中的cookie发送给服务器。
例如,这个Cookie: JSESSIONID=41C481F0224664BDB28E95081D23D5B8就会再次发送给服务器。
服务器就是根据41C481F0224664BDB28E95081D23D5B8这个值来找到对应的session对象的。
cookie怎么生成?cookie保存在什么地方?cookie有啥用?浏览器什么时候会发送cookie,发送哪些cookie给服务器?
cookie最终是保存在浏览器客户端上的。
可以保存在运行内存中。(浏览器只要关闭cookie就消失了。)
也可以保存在硬盘文件中。(永久保存。)
cookie有啥用呢?
cookie和session机制其实都是为了保存会话的状态。
cookie是将会话的状态保存在浏览器客户端上。(cookie数据存储在浏览器客户端上的。)
session是将会话的状态保存在服务器端上。(session对象是存储在服务器上。)
举个例子,我们经常会遇到保存密码免登录,我们想一想这个过程是怎么实现的
第一次,用户输入密码和账户,那么服务端就会获取到该用户的信息,通过对数据库的连接查询,账户和密码是否正确,如果正确,则证明登录成功,也就这么这次会话将会开启,服务端会返回一个sessionID也就是Cookie给浏览器,还需要再进行判断,用户是不是勾选了免登录,如果勾选了,我们需要将密码和账户封装成Cookie返回给浏览器。
第二次,用户再次访问服务端,会携带Cookie,这时候我们需要进行一次判断,判断Cookie中有没有封装的Cookie密码和账户两个信息,如果有,则取出这两个信息,再次进行数据库查询判断,如果没有,则要重新登录。
思路:
1.判断浏览器是否携带了密码和账户的Cookie信息
如果没有,则进行登录操作
如果有,则判断Cookie的账户密码信息是否正确,正确则登录到服务端,如果失败则重新登录
2.登录成功后返回一个session对象,保持通信,并判断是否用户是否勾选了免登录
如果有,则给浏览器返回封装的Cookie密码和账户
如果没有,则不会返回
代码如下
package com.movie.web.servlet;
import com.movie.bean.User;
import com.movie.utiles.DBUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@WebServlet("/welcome")
public class WelcomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取浏览器的cookie
//cookie可能有也可能没有,如果有,则其长度大于0
Cookie[] cookies = request.getCookies();
//用户账号
String username = null;
//用户密码
String password = null;
if (cookies != null){
for (Cookie cookie : cookies){
//获取cookie的名字
String name = cookie.getName();
if ("username".equals(name)){
//找到用户账号对应的数据
username = cookie.getValue();
}else if ("password".equals(name)){
//找到用户密码对应的数据
password = cookie.getValue();
}
}
}
//判断username和password是否正确
if (username != null && password != null){
//验证cookie对应的账号密码是否正确
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
//标记
boolean success = false;
try {
conn = DBUtil.getConnection();
String sql = "select c_name,c_password from c_user where c_name = ? and c_password = ?;";
ps = conn.prepareStatement(sql);
ps.setString(1,username);
ps.setString(2,password);
rs = ps.executeQuery();
if (rs.next()){
success = true;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}finally {
DBUtil.close(conn,ps,rs);
}
if (success){
//应该成功了就要获取session,session会超时销毁
HttpSession session = request.getSession();
//封装用户
User user = new User(username,password);
//将登录成功的用户放入session域
session.setAttribute("user",user);
//跳转主页面
response.sendRedirect(request.getContextPath()+"/record/list");
}else {
//跳转到登录页
response.sendRedirect(request.getContextPath()+"/index.jsp");
}
}else {
//证明cookie为空,跳转到登录页
response.sendRedirect(request.getContextPath()+"/index.jsp");
}
}
}
private void doLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {
//标记是否登录成功
boolean success = false;
String username = request.getParameter("username");
String password = request.getParameter("password");
String d = request.getParameter("d");
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
String sql;
String jump;
if ("1".equals(d)){
sql = "select u_name,u_password from t_user where u_name = ? and u_password = ?";
jump = "movie";
}else {
sql = "select c_name,c_password from c_user where c_name = ? and c_password = ?";
jump = "record";
}
try {
conn = DBUtil.getConnection();
ps = conn.prepareStatement(sql);
ps.setString(1,username);
ps.setString(2,password);
rs = ps.executeQuery();
if (rs.next()){
//这时证明登录成功
success = true;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}finally {
DBUtil.close(conn,ps,rs);
}
if (success){
//因为登录成功,所以要给此用户凭证证明已经登录
//所以必须获取到session,否则会被拦截
//且此seesion表示当前用户已成功登录
HttpSession session = request.getSession();
//将此用户存入session域中,前端显示
User user = new User(username,password);
session.setAttribute("user",user);
//登录成功,并且用户选择了“十天内免登录”
String f = request.getParameter("f");
if ("1".equals(f)){
//创建Cookie对象存储登录名与密码
Cookie cookie1 = new Cookie("username",username);
Cookie cookie2 = new Cookie("password",password);
//设置Cookie的有效时长
cookie1.setMaxAge(60 * 60 * 24 * 10);
cookie2.setMaxAge(60 * 60 * 24 * 10);
//设置Cookie的路径(只要访问该项目。浏览器就要携带这两个cookie)
cookie1.setPath(request.getContextPath());
cookie2.setPath(request.getContextPath());
//响应cookie给浏览器
response.addCookie(cookie1);
response.addCookie(cookie2);
}
//无论选择不选择免登录,最后都需要跳转到主页面
response.sendRedirect(request.getContextPath()+"/"+jump+"/list");
}else {
//失败,跳转到失败页面
response.sendRedirect(request.getContextPath()+"/error.jsp");
}
}
语法什么的我就不去详细说啦,希望大家能理解我的思路,首先我先获取浏览器请求所携带的所有Cookie,如果有账户和密码的内容,进行验证并跳转,如果没有,则跳转到登录界面。
登录界面对应的是登录servlet,首先先判断登录的是管理员还是普通用户,根据不同的权限去查询对应的表,然后判断是否登录成功,如果登录成功就要返回一个session,并判断是否选择了免登录,如果选择了则返回给浏览器一个Cookie对象,用于下次访问的判断,登录失败则跳转到失败页面,重新登录。
结尾
还有很多内容,我就不详细去说了,后面再说一下监听器与过滤器
这次web的学习我搭建的项目还是JavaSE总结的电影管理系统,对上一份代码进行了拓展和web的实现