JavaWeb开发(二)

完结篇?

MVC 架构模式

MVC(Model View Controller)是一种软件架构模式,将软件系统分为模型、视图和控制器三个部分,对应业务逻辑、数据和显示界面。

MVC的含义
MVC实例

DAO(Data Access Object)层又称“持久层”或“数据访问层”,作用是封装所有对数据库的访问逻辑、将数据库操作(增删改查)与业务逻辑彻底分离提供一组接口,让上层(Service 层)只需调用接口方法,无需关心底层如何与数据库交互。DAO层一般需要定义接口和实现类

POJO是 Plain Old Java Object 的缩写,意思是“普通的 Java 对象”。它通常指不依赖任何框架或特殊类继承/注解的、只包含属性、构造方法、Getter/Setter、toString() 等基本方法的实体类。

POJO的实体类的基本要求:

  1. 类名和表格名称应该对应
  2. 类的字段名和表格的列名应该对应
  3. 每个字段必须是私有的
  4. 每个属性都应该具备 getter setter
  5. 必须具备无参构造器
  6. 应该事先序列化接口(缓存、分布式项目数据传递,可能会将对象序列化)
  7. 应该重写类的hashcodeequals 方法
  8. toString 是否重写都可

lombok

一个小工具,可以通过注解的形式把 getter setter hashcode等模板方法直接补全。详见lombok官方指南

数据类相关:

注解 功能
@Getter 自动为所有字段生成 getXxx() 方法
@Setter 自动为所有字段生成 setXxx() 方法
@ToString 自动生成 toString() 方法
@EqualsAndHashCode 自动生成 equals()hashCode() 方法
@Data 综合注解,包含 @Getter@Setter@ToString@EqualsAndHashCode@RequiredArgsConstructor(对于 final 字段)

构造器相关:

注解 功能
@NoArgsConstructor 生成无参构造函数
@AllArgsConstructor 生成包含所有字段的构造函数
@RequiredArgsConstructor 生成包含所有 final 字段或带 @NonNull 字段的构造函数

构建器模式:

注解 功能
@Builder 提供链式构建对象方式
@Singular @Builder 配合,用于集合字段构建时避免重复添加逻辑

其他实用注解:

注解 功能
@Slf4j 自动为类引入 private static final org.slf4j.Logger log 日志对象(也有 @Log4j, @Log 等变种)
@SneakyThrows 自动捕获并抛出 checked 异常(不用写 try-catch)
@NonNull 在方法参数或字段上使用,自动生成空值检查
@Cleanup 自动调用 close(),常用于资源释放(如 InputStream

会话管理

概述

“会话管理”(Session Management)是指:在客户端与服务器之间的交互过程中,服务器如何识别并保持用户身份和状态信息

为什么需要会话管理?因为 HTTP 协议本身是无状态的协议,对于发送的请求和响应都不会进行保存,没有记忆功能。为了实现用户登录状态的记录、购物车、填写表单等具有记忆的功能,需要一种能够记忆用户状态的机制——会话管理。

使用

cookie 是一种客户端会话技术, cookie 由服务端产生,它是服务器存放在浏览器的一小份数据,浏览器以后每次访问该服务器的时候都会将这小份数据携带到服务器去。

  • 服务端创建cookie,将cookie放入响应对象中,Tomcat容器将cookie转化为set-cookie响应头,响应给客户端
  • 客户端在收到服务端发来的cookie的响应头后,在以后每次请求该服务的资源时,会以cookie请求头的形式携带之前收到的Cookie
  • cookie是一种键值对格式的数据,从tomcat8.5开始可以保存中文,但是不推荐
  • 由于cookie是存储于客户端的数据,比较容易暴露,一般不存储一些敏感或者影响安全的数据
原理图

创建cookie:

1
2
3
4
// 服务端创建 cookie
Cookie cookie = new Cookie("name", "Kurumi");
// 放入响应对象
resp.addCookie(cookie);

获取cookie:

1
2
3
4
5
6
7
8
9
// 获取请求中携带的cookie
Cookie[] cookies = req.getCookies();
// 请求中的多个cookie会进入该数组
// 如果没有携带任何cookie,数组为null,无法迭代
if(cookies != null) {
for(Cookie cookie : cookies) {
System.out.println(cookie.getName() + "=" + cookie.getValue());
}
}

时效性

默认情况下Cookie的有效期是一次会话范围内,关闭浏览器后Cookie就会消失,可以通过cookie的setMaxAge()方法让Cookie持久化保存到浏览器上

cookie.setMaxAge(int expiry)参数单位是秒,表示cookie的持久化时间,如果设置参数为0,表示将浏览器中保存的该cookie删除。

  • servletA设置一个Cookie为持久化cookie
1
2
3
4
5
6
7
8
9
10
11
12
13
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 创建Cookie
Cookie cookie1 =new Cookie("c1","c1_message");
cookie1.setMaxAge(60);
Cookie cookie2 =new Cookie("c2","c2_message");
// 将cookie放入响应对象
resp.addCookie(cookie1);
resp.addCookie(cookie2);
}
}

限制提交路径

访问互联网资源时不能每次都需要把所有Cookie带上。访问不同的资源时,可以携带不同的cookie,我们可以通过cookie的setPath(String path) 对cookie的路径进行设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 创建Cookie
Cookie cookie1 =new Cookie("c1","c1_message");
// 设置cookie的提交路径
cookie1.setPath("/web03_war_exploded/servletB");
Cookie cookie2 =new Cookie("c2","c2_message");
// 将cookie放入响应对象
resp.addCookie(cookie1);
resp.addCookie(cookie2);
}
}

Session

HttpSession是一种保留更多信息在服务端的一种技术,服务器会为每一个客户端开辟一块内存空间,即session对象. 客户端在发送请求时,都可以使用自己的session. 这样服务端就可以通过session来记录某个客户端的状态了。

和cookie的区别:

  • cookie是存储在客户端(浏览器)的小块文本数据,又服务器发送并保存在浏览器中,每次请求时浏览器都会自动把它们发送给服务器,用于找回服务器中保存的Session数据。
  • session是存储在服务器的数据,记录了用户的会话状态,比如登录信息、购物车内容等。
  • session的安全性更高,容量没有明确的限制;cookie的安全性低,容量一般不超过4kb。

Session 和 Cookie 的关系:

  • Session 依赖 Cookie 来实现识别用户身份。
  • 通常,服务器在创建一个 Session 时,会生成一个唯一的 Session ID。
  • 这个 Session ID 会通过 Set-Cookie 响应头被发送到客户端,客户端会把这个 ID 保存在 Cookie 中。
  • 后续客户端的请求会自动携带这个 Session ID 的 Cookie,服务器就可以用这个 ID 找回之前保存的 Session 数据,从而识别用户状态。

Session的原理图:

Session的原理图

示例:假如有一个表单name属性为username,现在使用ServeletA将表单提交的用户名存入Session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取请求中的参数
String username = req.getParameter("username");
// 获取session对象
HttpSession session = req.getSession();
// 获取Session的ID
String jSessionId = session.getId();
System.out.println(jSessionId);
// 判断session是不是新创建的session
boolean isNew = session.isNew();
System.out.println(isNew);
// 向session对象中存入数据
session.setAttribute("username",username);

}
}

使用getSession()方法可以获取当前会话的实例,获取的逻辑如下:

getSession方法的操作逻辑

这时返回给客户端的响应中包含一个键为JSESSIONID的cookie。

然后在另一个ServletB中就可以取出用户名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/servletB")
public class ServletB extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取session对象
HttpSession session = req.getSession();
// 获取Session的ID
String jSessionId = session.getId();
System.out.println(jSessionId);
// 判断session是不是新创建的session
boolean isNew = session.isNew();
System.out.println(isNew);
// 从session中取出数据
String username = (String)session.getAttribute("username");
System.out.println(username);
}
}

调用servletB时可以看到请求中也携带了一个JSESSIONID的cookie。

Session的时效性

默认的session最大闲置时间(两次使用同一个session中的间隔时间) 在tomcat/conf/web.xml配置为30分钟。

也可以通过HttpSession的API 对最大闲置时间进行手动设定

1
2
// 设置最大闲置时间
session.setMaxInactiveInterval(60);

也可以直接让session失效

1
2
// 直接让session失效
session.invalidate();

域对象

域对象是用于存储和传递数据的对象。

比较重要的域:请求域,会话域,应用域。

  • 请求域对象:HttpServletRequest,传递数据的范围是一次请求之内和多次转发。
  • 会话域对象:HttpSession,传递数据的范围是一次会话之内,同一个浏览器发起的请求可以共享该对象,但是换一个浏览器就不可以了。
  • 应用域:ServletContext,传递数据的范围是本应用之内。
请求域
会话域
应用域
所有域合在一起

过滤器

过滤器的工作位置:

过滤器的使用

日志记录

骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import jakarta.servlet.*;

import java.io.IOException;

public class LoggingFilter implements Filter {
// 请求到达之前先使用该方法。
// 可以在该方法内直接返回响应
// 返回响应之前还会调用该方法
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 1. 请求到达目标资源之前的功能代码
// 判断是否登录;校验权限是否满足等
// 2. 放行代码
// 3. 响应之前 HttpServletResponse 转换为响应消息之前的功能代码
}
}

传入参数中的FilterChain是一个接口,内部定义了一个方法:

1
2
3
public interface FilterChain {
void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}

注意这个doFilterLoggingFilter类重写的doFilter方法不是一个方法,这里没有递归。这里的doFilter方法的作用是:

在多个 Filter 之间传递请求和响应对象,或者最终将请求传递给目标 Servlet

放行代码

1
filterChain.doFilter(servletRequest, servletResponse);

为什么调用doFilter就是放行了?举个例子,假设我们有这样一条过滤器链:

1
Request --> FilterA --> FilterB --> FilterC --> Servlet

如果在FilterB中调用了chain.doFilter(request, response);,那么请求才会继续往下传,传到下一个过滤器。反之,如果不调用该方法,那么请求就会卡在过滤器B中,达到了拦截的效果。

通过设计doFilter方法,我们可以在过滤器中处理请求,经过仔细判断之后再决定要不要往下传。调用doFilter,则相当于通过过滤器,传给下一个过滤器或Servlet;不调用则不再往下传递,视为拦截。


还有一个问题:多个过滤器链式调用的时候,它们的先后顺序是怎么确定的?答案是web.xml 中的<filter-mapping>的前后顺序决定执行顺序。

如果采用 Servlet 3.0+ 的标准注解形式,那么执行顺序取决于类名在字典中的顺序。在使用 Spring 的情况下才可以通过注解明确指定执行顺序。

过滤器链图解

配置过滤器

方法一:通过web.xml配置

初始化过滤器;

1
2
3
4
<filter>
<filter-name>LoggingFilter</filter-name>
<filter-class>com.kzn.filters.LoggingFilter</filter-class>
</filter>

配置过滤器

1
2
3
4
5
<filter-mapping>
<filter-name>LoggingFilter</filter-name>
<servlet-name>Servlet1</servlet-name>
<!-- <url-pattern></url-pattern> -->
</filter-mapping>
  • servlet-name: 根据servlet的name属性过滤(这个属性的默认值为"",需要手动配置)
  • url-pattern: 根据请求的资源路径来过滤资源。具体规则:精确路径匹配/servlet, 路径匹配/*, 扩展名匹配*.action,默认匹配 / 优先级依次靠后。如果使用/*,说明所有的请求和响应都会经过这个过滤器。

两者不能混用,否则会导致同一个过滤器被错误地重复执行。

简易日志的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class LoggingFilter implements Filter {
// 获取日期对象
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

// 强转,方便获取uri
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;

String uri = httpRequest.getRequestURI();
// 日志的格式:yyyy-MM-dd HH:mm:ss 请求 耗时 xx 毫秒
String dateTime = dateFormat.format(new Date());
String beforeLogin = dateTime + " : " +uri + "was logged in";
// 输出日志
System.out.println(beforeLogin);

// 计时开始
long t1 = System.currentTimeMillis();

// 放行的代码
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("after filter invoked");

// 计时结束
long t2 = System.currentTimeMillis();

String afterLogin = uri + " has spent " + (t2 - t1) + " milliseconds";
// 输出日志
System.out.println(afterLogin);
}
}

过滤器的生命周期

过滤器作为web项目的组件之一,和Servlet的生命周期类似,略有不同,没有servlet的load-on-startup的配置,默认就是系统启动立刻构造

阶段 对应方法 执行时机 执行次数
创建对象 构造器 web应用启动时 1
初始化方法 void init(FilterConfig filterConfig) 构造完毕 1
过滤请求 void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) 每次请求 多次
销毁 default void destroy() web应用关闭时 1次

监听器

使用频率不如过滤器。

监听器:专门用于对域对象对象身上发生的事件或状态改变进行监听和相应处理的对象。

监听对象:域对象发生的事件,例如:域对象的创建、销毁,数据的修改、删除等,当这些事件发生时监听器执行相应的代码。

application listener

配置方式:

1
2
3
<listener>
<listener-class>com.kzn.Listener</listener-class>
</listener>

或者使用注解:@WebListener


如果想要监听应用域的初始化和销毁事件,需要实现ServletContextListener接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebListener
public class Listener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext servletContext = sce.getServletContext();
System.out.println(servletContext.hashCode()+" contextInitialized");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
ServletContext servletContext = sce.getServletContext();
System.out.println(servletContext.hashCode()+" contextDestroyed");
}
}

在应用启动和销毁时各执行一次.

如果监听 ServletContext 的参数的变化,需要实现ServletContextAttributeListener,一共三个抽象方法:

1
2
3
4
5
6
7
8
9
10
11
public interface ServletContextAttributeListener extends EventListener {
// 属性数量增加
default void attributeAdded(ServletContextAttributeEvent scae) {
}
// 属性被移除
default void attributeRemoved(ServletContextAttributeEvent scae) {
}
// 属性被修改
default void attributeReplaced(ServletContextAttributeEvent scae) {
}
}

会话域和请求域

和应用域完全类似,只不过调用的方法的ServletContext替换成SessionRequest

Session还有两个特殊的监听器:

  • HttpSessionBindingListener 监听当前监听器对象在Session域中的增加与移除
  • HttpSessionBindingEvent对象代表属性变化事件
  • HttpSessionActivationListener监听钝化和活化,将内存中的session序列化变成磁盘中的文件称之为钝化,反序列化变成内存中的对象称之为活化。

JavaWeb开发(二)
https://kznep19.blog/2025/05/14/Tomcat2/
作者
banyee
发布于
2025年5月14日
许可协议