Administrator
Published on 2025-04-19 / 15 Visits
0
0

JavaServlet学习笔记分享

1.Servlet概述

1.1.Web Server

HTTP协议

今天我们访问网站,使用App时,都是基于Web这种Browser/Server模式,简称B/S架构,它的特点是,客户端只需要一个浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取Web页面,并把Web页面展示给用户即可,在Web应用中,浏览器请求一个URL,服务器就把生成的HTML网页响应(发送)给浏览器,而浏览器和服务器之间的传输协议是HTTP,HTTP协议是一个基于TCP协议之上的请求-响应协议。

要实现服务器通过HTTP协议把各种数据和页面文件响应给浏览器,就需要我们实现和遵循HTTP传输规范协议,就需要开发一个能实现http协议的服务应用,我们把能实现http协议通信的服务应用称之为Web Server(web服务器)

HTTP协议目前有多个版本

HTTP 1.0是早期版本,浏览器每次建立TCP连接后,只发送一个HTTP请求并接收一个HTTP响应,然后就关闭TCP连接,

HTTP 1.1允许浏览器和服务器在同一个TCP连接上反复发送、接收多个HTTP请求和响应

HTTP 2.0可以支持浏览器同时发出多个请求,但每个请求需要唯一标识,服务器可以不按请求的顺序返回多个响应,由浏览器自己把收到的响应和请求对应起来

HTTP 3.0为了进一步提高速度,将抛弃TCP协议,改为使用无需创建连接的UDP协议,目前HTTP 3.0仍然处于实验阶段。

HTTP请求和响应都由HTTP Header和HTTP Body构成,其中HTTP Header每行都以\r\n结束。如果遇到两个连续的\r\n,那么后面就是HTTP Body。

一个浏览器http协议请求内容如下:

GET / HTTP/1.1
Host: www.sina.com.cn
User-Agent: Mozilla/5.0 xxx
Accept: */*
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8

一个服务器http响应内容如下

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 21932
Content-Encoding: gzip
Cache-Control: max-age=300

<html>...网页数据...

浏览器读取HTTP Body,并根据Header信息中指示的Content-TypeContent-Encoding等解压后显示网页、图像或其他内容,然后再展示渲染到浏览器窗口当中。

编写Web Server(web服务器)

以创建一个仅支持Http传输协议的简单web服务器为例:

一个HTTP Server本质上是一个TCP服务器,我们先用**TCP编程(Java网络编程)**的多线程实现的服务器端框架。

处理HTTP请求核心代码是,先读取HTTP请求,这里我们只处理GET /的请求。当读取到空行时,表示已读到连续两个\r\n,说明请求结束,可以发送响应。发送响应的时候,首先发送响应代码HTTP/1.0 200 OK表示一个成功的200响应,使用HTTP/1.0协议,然后,依次发送Header,发送完Header后,再发送一个空行标识Header结束,紧接着发送HTTP Body,在浏览器输入http://127.0.0.1:8080/就可以看到响应页面:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class myHttp {
	public static void main(String[] args) throws IOException {
		ServerSocket ss = new ServerSocket(8080); // 监听指定端口
		System.out.println("server is running...");
		for (;;) {
			Socket sock = ss.accept();
			System.out.println("connected from " + sock.getRemoteSocketAddress());
			Thread t = new Handler(sock);
			t.start();
		}
	}
}
class Handler extends Thread {
	Socket sock;

	public Handler(Socket sock) {
		this.sock = sock;
	}

	public void run() {
		try (InputStream input = this.sock.getInputStream()) {
			try (OutputStream output = this.sock.getOutputStream()) {
				handle(input, output);
			}
		} catch (Exception e) {
		} finally {
			try {
				this.sock.close();
			} catch (IOException ioe) {
			}
			System.out.println("client disconnected.");
		}
	}

	private void handle(InputStream input, OutputStream output) throws IOException {
		var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
		var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
		//处理HTTP请求核心代码如下
		boolean requestOk = false;
		String first = reader.readLine();
		if (first.startsWith("GET / HTTP/1.")) {
			requestOk = true;
		}
		for (;;) {
			String header = reader.readLine();
			if (header.isEmpty()) { // 读取到空行时, HTTP Header读取完毕
				break;
			}
			System.out.println(header);
		}
		System.out.println(requestOk ? "Response OK" : "Response Error");
		if (!requestOk) {
			// 发送错误响应:
			writer.write("HTTP/1.0 404 Not Found\r\n");
			writer.write("Content-Length: 0\r\n");
			writer.write("\r\n");
			writer.flush();
		} else {
			// 发送成功响应:
			String data = "<html><body><h1>Hello, world!</h1></body></html>";
			int length = data.getBytes(StandardCharsets.UTF_8).length;
			writer.write("HTTP/1.0 200 OK\r\n");
			writer.write("Connection: close\r\n");
			writer.write("Content-Type: text/html\r\n");
			writer.write("Content-Length: " + length + "\r\n");
			writer.write("\r\n"); // 空行标识Header和Body的分隔
			writer.write(data);
			writer.flush();
		}
	}
}

运行代码并在浏览器中访问:127.0.0.1:8080 结果如下

image-20250415172600950

这样实现了简单的支持HTTP的**Web Server(web服务器)**了

1.2.Servlet

在Web Server章节中,我们看到,编写HTTP服务器其实是非常简单的,只需要先编写基于多线程的TCP服务,然后在一个TCP连接中读取HTTP请求,发送HTTP响应即可。

但是,要编写一个完善的HTTP服务器,以HTTP/1.1为例,需要考虑的包括:

  • 识别正确和错误的HTTP请求;
  • 识别正确和错误的HTTP头;
  • 复用TCP连接;
  • 复用线程;
  • IO异常处理;
  • ...

这些基础工作需要耗费大量的时间,并且经过长期测试才能稳定运行。如果我们只需要输出一个简单的HTML页面,就不得不编写上千行底层代码,那就根本无法做到高效而可靠地开发

因此,在JavaEE平台上,处理TCP连接,解析HTTP协议这些底层工作统统扔给现成的Web服务器去做,我们只需要把自己的应用程序跑在Web服务器上。为了实现这一目的,JavaEE提供了Servlet API,只要Web服务器实现了Servlet API接口,我们就可以使用Servlet API编写自己的Servlet来处理HTTP请求。

┌───────────┐
                 │My Servlet │
                 ├───────────┤
                 │Servlet API│
┌───────┐  HTTP  ├───────────┤
│Browser│◀──────▶│Web Server │
└───────┘        └───────────┘

在市场上有许多 Web 应用服务器支持 Servlet,例如:

  • **Tomcat:**Apache Tomcat是最知名的开源Servlet容器之一。它以轻量级和易用性而闻名,适用于中小型Web应用程序。Tomcat提供了稳定的性能和丰富的文档资源,使其成为许多Java开发人员的首选。此外,Tomcat还支持JavaServer Pages(JSP)和JavaServer Faces(JSF)等其他Java技术。
  • **Jetty:**Jetty是另一个轻量级的开源Servlet容器,以其速度和嵌入式特性而著名。Jetty适用于嵌入式应用程序,可以轻松嵌入到Java应用程序中。它还支持HTTP/2和WebSocket,适用于构建实时和高性能的Web应用程序。
  • **WildFly(以前称为JBoss):**WildFly是一个强大的开源应用服务器,同时也是一个Servlet容器。它支持Java EE(Enterprise Edition)规范,具有丰富的功能集,适用于构建复杂的企业级应用程序。WildFly具有高度可扩展性和性能,是Java EE应用程序的首选容器。
  • **GlassFish:**GlassFish是另一个开源的Java EE应用服务器,也是一个Servlet容器。它由Eclipse Foundation支持,具有强大的功能和性能。GlassFish易于配置,并且适用于大型企业应用。
  • **WebLogic:**WebLogic是Oracle的商业级Java EE应用服务器,它也包括Servlet容器。虽然它是一个商业产品,但它在企业级应用程序领域具有广泛的应用。WebLogic提供了高级的管理和监控功能,以满足大型和复杂应用程序的需求。
  • **Resin:**Caucho Resin是一个高性能的商业Servlet容器,以其速度和可伸缩性而著名。它支持Java EE和Spring等框架,适用于高负载的Web应用程序

因为Apache Tomcat(web应用服务器)是最知名的开源Servlet容器之一,因为它免费开源,且大部分公司的中小型JavaWeb应用都使用Apache Tomcat做为Servlet的运行容器。

Servlet版本

要务必注意servlet-api的版本。4.0及之前的servlet-api由Oracle官方维护,引入的依赖项是javax.servlet:javax.servlet-api,编写代码时引入的包名为:

import javax.servlet.*;
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

而5.0及以后的servlet-api由Eclipse开源社区维护,引入的依赖项是jakarta.servlet:jakarta.servlet-api,编写代码时引入的包名为:

import jakarta.servlet.*;
<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>5.0.0</version>
    <scope>provided</scope>
</dependency>

Tomcat版本

由于Servlet版本分为<=4.0和>=5.0两种,所以,要根据使用的Servlet版本选择正确的Tomcat版本。从Tomcat版本页可知:

  • 使用Servlet<=4.0时,选择Tomcat 9.x或更低版本;
  • 使用Servlet>=5.0时,选择Tomcat 10.x或更高版本。

运行本节代码需要使用Tomcat>=10.x版本。

在Servlet容器中运行的Servlet具有如下特点:

  • 无法在代码中直接通过new创建Servlet实例,必须由Servlet容器自动创建Servlet实例;
  • Servlet容器只会给每个Servlet类创建唯一实例;
  • Servlet容器会使用多线程执行doGet()doPost()方法。

2.创建JavaServlet项目

JavaServlel配合Tomcat服务器就是我们所说的JavaWeb服务了

2.1.使用Java Enterprise创建创建

1)File-new-project

选中Java Enterprise更改Project template为Web application点击下一步

这种方式其实也是通过Maven的模板创建的

image-20250415175957599

image-20250415180222652

创建完成后的目录结果如下。

image-20250415180557020

2)设置tomcat运行环境(默认不需要操作)

选择Run --> Edit configuration

默认情况下创建的javaweb项目的tomcat运行环境如下,如何不同可以下面教程设置

image-20250415182230637

如果你的的运行环境为空,如下图,就需设置tomcat运行环境

image-20250415183018988

还是在上面那个界面,点击+加号,选中Tomcat Server下的Local,

image-20250415182641705

image-20250415183253628

image-20250415183329776

然后就可以运行看结果了

image-20250415183505596

image-20250415183619912

2.2.使用maven骨架模板创建

1)File --> new --> project -->meven

image-20250415184636274

image-20250415184832747

image-20250415184857906

创建完成的项目结构如下

image-20250415185119719

在main目录中右键创建java目录并设置为Sources Root

image-20250415185934003

将webapp目录下Web-INF下不要的applicationContext.xmllog4j.xml删除。

将web.xml中的filter全部删除,保留如下内容

<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
</web-app>

将pom.xml中默认的构建插件删除,并添加servlet依赖,保留pom.xml文件内容如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <packaging>war</packaging>
  
  <name>Maven-Template-Create</name>
  <groupId>org.example</groupId>
  <artifactId>Maven-Template-Create</artifactId>
  <version>1.0-SNAPSHOT</version>
  
  <dependencies>
    <!--servlet依赖-->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
  
  
</project>

2)设置tomcat运行环境

选择Run --> Edit configuration

默认情况下创建的javaweb项目的tomcat运行环境如下,如何不同可以下面教程设置

image-20250415182230637

如果你的的运行环境为空,如下图,就需设置tomcat运行环境

image-20250415183018988

还是在上面那个界面,点击+加号,选中Tomcat Server下的Local,

image-20250415182641705

image-20250415183253628

image-20250415183329776

在java目录下创建一个HelloServlet.java文件,内容如下

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;

@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
	private String message;

	public void init() {
		message = "Hello World!";
	}

	public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
		response.setContentType("text/html");

		// Hello
		PrintWriter out = response.getWriter();
		out.println("<html><body>");
		out.println("<h1>" + message + "</h1>");
		out.println("</body></html>");
	}

	public void destroy() {
	}
}

在webapp下创建index.jsp,内容如下

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>

然后就可以运行看结果了

image-20250415183505596

结果如下

image-20250415183619912

2.3.使用maven直接创建

1)在File --> new --> project --> meven

不用模板 直接创建 直接点击下一步

image-20250415210230120

image-20250415210402766

创建后的项目结构如下

image-20250415210535634

2)在main文件夹下创建webapp、在webapp下创建index.jsp文件和WEB-INF目录、在WEB-INF目录下创建web.xml

创建后结构如下

image-20250415210852675

3)选择file --> Project Structure

image-20250415211354201

在弹出来的对话框中选择我们创建的项目

image-20250415211450121

更改Deployment Descriptors中的path路径web.xml的路径

更改Web Resource Directories中的路径为创建的webapp的路径

点击Apply应用

image-20250415212239783

4)选择 file --> Project Structure ---> Artifacts

点击 加号+ 选择WebApplication:Exploded指向创建的项目

点击 加号+ 选择WebApplication:Artifacts指向创建的项目

image-20250415213048158

5)修改pox.xml文件,内容如下,刷新依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <name>Maven-Template-Create</name>
  <groupId>org.example</groupId>
  <artifactId>Maven-Template-Create</artifactId>
  <version>1.0-SNAPSHOT</version>

  <!--打包为war包-->
  <packaging>war</packaging>
  <!--添加servlet依赖-->
  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>


</project>

修改web.xml文件,内容如下

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
</web-app>

修改index.jsp文件,内容如下

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>

在java目录下创建HelloServlet.java文件,内容如下

import javax.servlet.annotation.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
   private String message;

   public void init() {
      message = "Hello World!";
   }

   public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
      response.setContentType("text/html");

      // Hello
      PrintWriter out = response.getWriter();
      out.println("<html><body>");
      out.println("<h1>" + message + "</h1>");
      out.println("</body></html>");
   }

   public void destroy() {
   }
}

6)设置tomcat运行环境

选择Run --> Edit configuration

点击+加号,选中Tomcat Server下的Local,

image-20250415182641705

image-20250415183253628

image-20250415183329776

然后就可以运行看结果了

image-20250415183505596

结果如下

image-20250415183619912

3.使用Servlet

Servlet官方文档

要想Java类处理Http请求,就必须让Java类继承Servlet的HttpServlet类,并继承HttpServlet的方法。

加载Servlet有两种方式,一种使用web.xml配,另一种是使用@WebServlet注解方式,两种方式选择其中一种就可以了

3.1.web.xml方式加载Servlet类

要想Java类处理Http请求,就必须让Java类继承Servlet的HttpServlet类,并继承HttpServlet的方法。

在java中创建一个test包,然后在test包中创建一个test类,内容如下

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class Test extends HttpServlet {

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException {
		// Test
		response.setContentType("text/html;charset=utf-8");
		PrintWriter out = response.getWriter();
		out.println("<html><body>");
		out.println("<h1>Test页面</h1>");
		out.println("</body></html>");
		out.flush();
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException {
		
	}

	@Override
	public void destroy() {
		
	}

	@Override
	public void init() throws ServletException {
		
	}
}

在web.xml中编写标签加载test类,内容如下

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <!--将Test类注册为servlet-->
    <servlet>
        <!--servlet名称,要求唯一-->
        <servlet-name>Test</servlet-name>
        <!--test类的全路径-->
        <servlet-class>test.Test</servlet-class>
        <!--加载顺序-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <!--请求路径映射-->
    <servlet-mapping>
        <!--关联servlet名称,一对一-->
        <servlet-name>Test</servlet-name>
        <!--url访问路径-->
        <url-pattern>/test</url-pattern>
    </servlet-mapping>
</web-app>

访问:http://localhost:8080/Java_Maven_Create_war_exploded/test 结果如下

image-20250415225203505

3.2.@WebServlet注解方式加载Servlet

要想Java类处理Http请求,就必须让Java类继承Servlet的HttpServlet类,并继承HttpServlet的方法。

在java中创建一个test包,然后在test包中创建一个test类,内容如下

@WebServlet(name = "myTest",value = "/test")中的name是servlet唯一名字,value是url资源映射路径

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;
@WebServlet(name = "myTest",value = "/test")
public class Test extends HttpServlet {

   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException {
      // Test
      response.setContentType("text/html;charset=utf-8");
      PrintWriter out = response.getWriter();
      out.println("<html><body>");
      out.println("<h1>Test页面</h1>");
      out.println("</body></html>");
      out.flush();
   }

   @Override
   protected void doPost(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException   {

   }

   @Override
   public void destroy() {

   }

   @Override
   public void init() throws ServletException {

   }
}

访问:http://localhost:8080/Java_Maven_Create_war_exploded/test 结果如下

image-20250415225203505

3.3.Servlet生命周期

返回值类型 方法名
void destroy()由servlet容器调用,以指示servlet正在退出服务。
ServletConfig getServletConfig()返回一个ServletConfig对象,其中包含此servlet的初始化和启动参数。
java.lang.String getServletInfo()返回有关servlet的信息,如作者、版本和版权。
void init(ServletConfig)由servlet容器调用,以指示servlet正在被放入服务中。
void service(ServletRequest req, ServletResponse res)由servlet容器调用,以允许servlet响应请求。

Servlet中的生命周期方法:

1)Servlet被创建时:

​ 执行init()方法,只执行一次

2)Servlet提供服务:执行service方法,执行多次

​ 每次访问Servlet时,Service方法都会被调用一次。

3)被销毁:执行destroy()方法,只执行一次次

​ Servlet被销毁时执行。服务器关闭时,Servlet被销毁

​ 只有服务器正常关闭时,才会执行destroy方法。

​ destroy()方法在Servlet被销毁之前执行,- -般用于释放资源

3.4.doGet和doPost方法中的参数

HttpServletRequest封装了一个HTTP请求,它实际上是从ServletRequest继承而来。最早设计Servlet时,设计者希望Servlet不仅能处理HTTP,也能处理类似SMTP等其他协议,因此,单独抽出了ServletRequest接口,但实际上除了HTTP外,并没有其他协议会用Servlet处理,所以这是一个过度设计。

我们从HttpServlet类当中继承的doGet和doPost方法参数如下,

doGet(HttpServletRequest req, HttpServletResponse response)

doPost(HttpServletRequest req, HttpServletResponse response)

3.4.1.HttpServletRequest 对象

我们通过HttpServletRequest提供的接口方法可以拿到HTTP请求的几乎全部信息,常用的方法有:

  • getMethod():返回请求方法,例如,"GET""POST"
  • getRequestURI():返回请求路径,但不包括请求参数,例如,"/hello"
  • getQueryString():返回请求参数,例如,"name=Bob&a=1&b=2"
  • getParameter(name):返回请求参数,GET请求从URL读取参数,POST请求从Body中读取参数;
  • getContentType():获取请求Body的类型,例如,"application/x-www-form-urlencoded"
  • getContextPath():获取当前Webapp挂载的路径,对于ROOT来说,总是返回空字符串""
  • getCookies():返回请求携带的所有Cookie;
  • getHeader(name):获取指定的Header,对Header名称不区分大小写;
  • getHeaderNames():返回所有Header名称;
  • getInputStream():如果该请求带有HTTP Body,该方法将打开一个输入流用于读取Body;
  • getReader():和getInputStream()类似,但打开的是Reader;
  • getRemoteAddr():返回客户端的IP地址;
  • getScheme():返回协议类型,例如,"http""https"

此外,HttpServletRequest还有两个方法:setAttribute()getAttribute(),可以给当前HttpServletRequest对象附加多个Key-Value,相当于把HttpServletRequest当作一个Map<String, Object>使用。

3.4.2.HttpServletResponse对象

HttpServletResponse封装了一个HTTP响应。由于HTTP响应必须先发送Header,再发送Body,所以,操作HttpServletResponse对象时,必须先调用设置Header的方法,最后调用发送Body的方法。

写入响应时,需要通过getOutputStream()获取写入流,或者通过getWriter()获取字符流二者只能获取其中一个

getOutputStream()和getWriter()方法的调用其实就是设置的http响应中的body内容

常用的设置Header的方法有:

  • setStatus(sc):设置响应代码,默认是200
  • setContentType(type):设置Body的类型,例如,"text/html"
  • setCharacterEncoding(charset):设置字符编码,例如,"UTF-8"
  • setHeader(name, value):设置一个Header的值;
  • addCookie(cookie):给响应添加一个Cookie;
  • addHeader(name, value):给响应添加一个Header,因为HTTP协议允许有多个相同的Header;

写入响应时,需要通过getOutputStream()获取写入流,或者通过getWriter()获取字符流二者只能获取其中一个

写入响应前,无需设置setContentLength(),因为底层服务器会根据写入的字节数自动设置,如果写入的数据量很小,实际上会先写入缓冲区,如果写入的数据量很大,服务器会自动采用Chunked编码让浏览器能识别数据结束符而不需要设置Content-Length头。

但是,写入完毕后调用flush()却是必须的,因为大部分Web服务器都基于HTTP/1.1协议,会复用TCP连接。如果没有调用flush(),将导致缓冲区的内容无法及时发送到客户端。此外,写入完毕后千万不要调用close(),原因同样是因为会复用TCP连接,如果关闭写入流,将关闭TCP连接,使得Web服务器无法复用此TCP连接。

3.4.3.Redirect重定向

重定向是指当浏览器请求一个URL时,服务器返回一个重定向指令,告诉浏览器地址已经变了,麻烦使用新的URL再重新发送新请求。

@WebServlet(urlPatterns = "/sr")
public class RedirectServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 构造重定向的路径:
        String redirectToUrl="/Java_Maven_Create_war_exploded"
        // 发送重定向响应:
        resp.sendRedirect(redirectToUrl);
    }
}

3.4.4.Forward转发

Forward是指内部转发。当一个Servlet处理请求的时候,它可以决定自己不继续处理,而是转发给另一个Servlet处理。

@WebServlet(urlPatterns = "/morning")
public class ForwardServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getRequestDispatcher("/hello").forward(req, resp);
    }
}

3.4.5.Session会话

我们经常要跟踪用户身份。当一个用户登录成功后,如果他继续访问其他页面,Web程序如何才能识别出该用户身份?

因为HTTP协议是一个无状态协议,即Web应用程序无法区分收到的两个HTTP请求是否是同一个浏览器发出的。为了跟踪用户状态,服务器可以向浏览器分配一个唯一ID(Session),并以Cookie的形式发送到浏览器,浏览器在后续访问时总是附带此Cookie,这样,服务器就可以识别用户身份。

我们把这种基于唯一ID识别用户身份的机制称为Session

每个用户第一次访问服务器后,会自动获得一个Session ID。如果用户在一段时间内没有访问服务器,那么Session会自动失效,下次即使带着上次分配的Session ID访问,服务器也认为这是一个新用户,会分配新的Session ID。

通过Session模拟登录注销

@WebServlet(urlPatterns = "/signin")
public class SignInServlet extends HttpServlet {
    // 模拟一个数据库:
    private Map<String, String> users = Map.of("bob", "bob123", "alice", "alice123", "tom", "tomcat");

    // GET请求时显示登录页:
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.setContentType("text/html");
         //设置编码
		resp.setCharacterEncoding("UTF-8");
		PrintWriter pw = resp.getWriter();
		pw.write("<h1>Sign In</h1>");
		pw.write("<form action=\"/signin\" method=\"post\">");
		pw.write("<p>Username: <input name=\"username\"></p>");
		pw.write("<p>Password: <input name=\"password\" type=\"password\"></p>");
		pw.write("<p><button type=\"submit\">登录</button> <a href=\"/index\">取消</a></p>");
		pw.write("</form>");
		pw.flush();
    }

    // POST请求时处理用户登录:
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("username");
        String password = req.getParameter("password");
        String expectedPassword = users.get(name.toLowerCase());
        if (expectedPassword != null && expectedPassword.equals(password)) {
            // 登录成功后,获取Session,并设置Session键值对
            req.getSession().setAttribute("user", name);
            //重定向到根路径"/index"
            resp.sendRedirect("/index");
        } else {
            resp.sendError(HttpServletResponse.SC_FORBIDDEN);
        }
    }
}
@WebServlet(urlPatterns = "/index")
public class IndexServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 从HttpSession获取当前用户名:
        String user = (String) req.getSession().getAttribute("user");
        resp.setContentType("text/html");
        //设置编码
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("X-Powered-By", "JavaEE Servlet");
        PrintWriter pw = resp.getWriter();
        pw.write("<h1>Welcome, " + (user != null ? user : "Guest") + "</h1>");
        if (user == null) {
            // 未登录,显示登录链接:
            pw.write("<p><a href=\"/signin\">登录/a></p>");
        } else {
            // 已登录,显示登出链接:
            pw.write("<p><a href=\"/signout\">退出登录</a></p>");
        }
        pw.flush();
    }
}
@WebServlet(urlPatterns = "/signout")
public class SignOutServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 从HttpSession移除用户名:
        req.getSession().removeAttribute("user");
        resp.sendRedirect("/index");
    }
}

3.4.6.Cookie会话

Servlet提供的HttpSession本质上就是通过一个名为JSESSIONID的Cookie来跟踪用户会话的。除了这个名称外,其他名称的Cookie我们可以任意使用。

如果我们想要设置一个Cookie,例如,记录用户选择的语言,可以编写一个LanguageServlet

@WebServlet(urlPatterns = "/pref")
public class LanguageServlet extends HttpServlet {

    private static final Set<String> LANGUAGES = Set.of("en", "zh");

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String lang = req.getParameter("lang");
        if (LANGUAGES.contains(lang)) {
            // 创建一个新的Cookie:
            Cookie cookie = new Cookie("lang", lang);
            // 该Cookie生效的路径范围:
            cookie.setPath("/");
            // 该Cookie有效期:
            cookie.setMaxAge(8640000); // 8640000秒=100天
            // 将该Cookie添加到响应:
            resp.addCookie(cookie);
        }
        resp.sendRedirect("/index");
    }
}

访问:http://localhost:8080/pref?lang=zh,设置Cookie

此,务必注意:浏览器在请求某个URL时,是否携带指定的Cookie,取决于Cookie是否满足以下所有要求:

  • URL前缀是设置Cookie时的Path;
  • Cookie在有效期内;
  • Cookie设置了secure时必须以https访问。

cookie

如果我们要读取Cookie,例如,在IndexServlet中,读取名为lang的Cookie以获取用户设置的语言,可以写一个方法如下:

private String parseLanguageFromCookie(HttpServletRequest req) {
    // 获取请求附带的所有Cookie:
    Cookie[] cookies = req.getCookies();
    // 如果获取到Cookie:
    if (cookies != null) {
        // 循环每个Cookie:
        for (Cookie cookie : cookies) {
            // 如果Cookie名称为lang:
            if (cookie.getName().equals("lang")) {
                // 返回Cookie的值:
                System.out.println(cookie.getValue());
                return cookie.getValue();
            }
        }
    }
    // 返回默认值:
    return "en";
}

3.4.7.总结

写入响应时,需要通过getOutputStream()获取写入流,或者通过getWriter()获取字符流,二者只能获取其中一个。

getOutputStream()和getWriter()方法的调用其实就是设置的响应http协议中的body内容

每个用户第一次访问服务器后,会自动获得一个Session ID,这个HttpSession本质上就是通过一个名为JSESSIONID的Cookie

3.5.Filter过滤器

Filter是一种对HTTP请求进行预处理的组件,它可以构成一个处理链,使得公共处理代码能集中到一起,它可以对资源(servlet或静态内容)的请求或对来自资源的响应执行过滤任务,或者对两者都执行过滤任务。主要用于对所有的请求和响应进行处理加工(请求和响应都会执行filter),例如:编码设置、认证识别等。

Filter适用于日志、登录检查、全局设置等;

设计合理的URL映射可以让Filter链更清晰。

3.5.1.Filter简单使用

一个类要成为filter就需要实现Filter接口及其方法,Fileter和Servlet一样有注解方式和web.xml配置方式,下面给出的所有例子是基于@WebFilter注解方式配置的。

例如:一个论坛应用程序:

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
               /             ┌──────────────┐
            │ ┌─────────────▶│ IndexServlet │ │
              │              └──────────────┘
            │ │/signin       ┌──────────────┐ │
              ├─────────────▶│SignInServlet │
            │ │              └──────────────┘ │
              │/signout      ┌──────────────┐
┌───────┐   │ ├─────────────▶│SignOutServlet│ │
│Browser├─────┤              └──────────────┘
└───────┘   │ │/user/profile ┌──────────────┐ │
              ├─────────────▶│ProfileServlet│
            │ │              └──────────────┘ │
              │/user/post    ┌──────────────┐
            │ ├─────────────▶│ PostServlet  │ │
              │              └──────────────┘
            │ │/user/reply   ┌──────────────┐ │
              └─────────────▶│ ReplyServlet │
            │                └──────────────┘ │
             ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─

各个Servlet设计功能如下:

  • IndexServlet:浏览帖子;
  • SignInServlet:登录;
  • SignOutServlet:登出;
  • ProfileServlet:修改用户资料;
  • PostServlet:发帖;
  • ReplyServlet:回复。

其中,ProfileServlet、PostServlet和ReplyServlet都需要用户登录后才能操作,否则,应当直接跳转到登录页面。

所以我们可以写一个Filter识别用户是否已经登录系统

@WebFilter("/user/*") //对/user/*的所有请求有效
public class AuthFilter implements Filter { //实现Filter
    //实现Filter接口方法
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("AuthFilter: check authentication");
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        if (req.getSession().getAttribute("user") == null) {
            // 未登录,自动跳转到登录页:
            System.out.println("AuthFilter: not signin!");
            resp.sendRedirect("/signin");
        } else {
            // 已登录,放行请求(交由下一个filter)继续处理:
            chain.doFilter(request, response);
        }
    }
}

因为字符是有编码的,字符编码不匹配会导致字符乱码,所以对字符编码进行处理时非常有必要的。

之前我们返回html代码代码有中文字符,响应时我们都会设置编码为UTF=8,所以每次要响应html代码时我们每次都要设置,现在有了Filter后我们将请求和响应都统一设置编码,这样我们只需要写一次设置编码,就可以任何地方都是生效

编写写一个设置编码的Filter

@WebFilter(urlPatterns = "/*") //对/*的所有请求有效
public class EncodingFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("EncodingFilter:doFilter");
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8"); //写这里和写在chain.doFilter后面的区别
        chain.doFilter(request, response);//会一直先后处理,如果还有filter继续处理,直到Servlet类
        response.setCharacterEncoding("UTF-8");//当Servlet类处理完成后,才开始调用doFilter后面的代码,处理响应字符编码
    }
}

现在我们设置了两个filter现在的调用链就变成了下面这样

┌────────┐
                                     ┌─▶│ServletA│
                                     │  └────────┘
    ┌──────────────┐    ┌─────────┐  │  ┌────────┐
───▶│EncodingFilter│───▶│LogFilter│──┼─▶│ServletB│
    └──────────────┘    └─────────┘  │  └────────┘
                                     │  ┌────────┐
                                     └─▶│ServletC│
                                        └────────┘

调用chain.doFilter()是继续放行请求,如果还有处理该请求的filter继续交由filter处理,直到所有Filter处理完成,才到Servlet类处理请求,当Servlet类处理完请求后,才一次执行每个Filter中chain.doFilter()后面的代码,如果处理该请求的Filter中有没有调用chain.doFilter(),那该请求是永远不能到达Servlet类

3.5.2.Filter使用总结

Filter是一种对HTTP请求进行预处理的组件,它可以构成一个处理链,使得公共处理代码能集中到一起

chain.doFilter()是继续放行请求,如果还有处理该请求的filter继续交由filter处理,直到所有filter处理完成,才到Servlet类处理请求,当Servlet类处理完请求后,才一次执行每个filter中chain.doFilter()后面的代码,如果处理该请求的filter中有任何一个每调用chain.doFilter(),那该请求是永远不可能到达Servlet类的。

3.6.Listener监听器

Listener顾名思义就是监听器,有好几种Listener,其中最常用的是ServletContextListener,我们编写一个实现了ServletContextListener接口的类如下:

@WebListener
public class AppListener implements ServletContextListener {
    // 在此初始化WebApp,例如打开数据库连接池等:
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("WebApp initialized.");
    }

    // 在此清理WebApp,例如关闭数据库连接池等:
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("WebApp destroyed.");
    }
}

任何标注为@WebListener,且实现了特定接口的类会被Web服务器自动初始化。

它会在整个Web应用程序初始化完成后调用contextInitialized(),以及Web应用程序关闭后contextDestroyed(),Web服务器保证在contextInitialized()执行后,才会接受用户的HTTP请求。很多第三方Web框架都会通过一个ServletContextListener接口初始化自己

除了ServletContextListener外,还有几种Listener:

  • HttpSessionListener:监听HttpSession的创建和销毁事件;
  • ServletRequestListener:监听ServletRequest请求的创建和销毁事件;
  • ServletRequestAttributeListener:监听ServletRequest请求的属性变化事件(即调用ServletRequest.setAttribute()方法);
  • ServletContextAttributeListener:监听ServletContext的属性变化事件(即调用ServletContext.setAttribute()方法);

3.6.1ServletContext

一个Web服务器可以运行一个或多个WebApp,对于每个WebApp,Web服务器都会为其创建一个全局唯一的ServletContext实例,我们在AppListener里面编写的两个回调方法实际上对应的就是ServletContext实例的创建和销毁:

public void contextInitialized(ServletContextEvent sce) {
    System.out.println("WebApp initialized: ServletContext = " + sce.getServletContext());
}

ServletRequestHttpSession等很多对象也提供getServletContext()方法获取到同一个ServletContext实例。ServletContext实例最大的作用就是设置和共享全局信息。

此外,ServletContext还提供了动态添加Servlet、Filter、Listener等功能,它允许应用程序在运行期间动态添加一个组件,虽然这个功能不是很常用。

4.手写MVC框架

MVC=ModelAndView(数据和视图)+Controller(由Servlet控制层实现)

Controller的方法定义的@Get@Post的Path决定调用哪个方法,最后,获得方法返回的ModelAndView后,渲染模板,写入HttpServletResponse,即完成了整个MVC的处理。

这个MVC的架构如下:

HTTP Request    ┌─────────────────┐
──────────────────▶│DispatcherServlet│
                   └─────────────────┘
                            │
               ┌────────────┼────────────┐
               ▼            ▼            ▼
         ┌───────────┐┌───────────┐┌───────────┐
         │Controller1││Controller2││Controller3│
         └───────────┘└───────────┘└───────────┘
               │            │            │
               └────────────┼────────────┘
                            ▼
   HTTP Response ┌────────────────────┐
◀────────────────│render(ModelAndView)│
                 └────────────────────┘

其中,DispatcherServlet以及如何渲染均由MVC框架实现,在MVC框架之上只需要编写每一个Controller,做到类似于下面这这种效果

@Controler
public class UserController {
    @GetMapping("/signin")
    public ModelAndView signin() {
        ...
        retrun new ModelAndView();
    }

    @PostMapping("/signin")
    public ModelAndView doSignin(SignInBean bean) {
        ...
        retrun new ModelAndView();
    }

    @GetMapping("/signout")
    public ModelAndView signout(HttpSession session) {
        ...
        retrun new ModelAndView();
    }
}

4.1.实现思路

MVC=ModelAndView(数据和视图)+Controller(由Servlet控制层实现)

要实现MVC就需要让ModelAndView和Controller受MVC框管理和控制,如何让MVC管理和控制,可以通过以下步骤实现:

第一:接管所有请求,可以通过编写一个Sevlet类并设置为接收根请求(“/”),这里我创建的是一个名为ServletDispatcher的Servlet类,如下

@WebServlet("/")
public class ServletDispatcher extends HttpServlet {
    //代码省略
}

第二:管理和控制ModelAndView,ModelAndView其实就是将HTML模板(View)和数据模型(View)进行关联和渲染,这些都可以使用第三方的模板引擎(Thymeleaf、FreeMarker、Velocity、pebbletemplates等)来做具体实现,我们要做的就是通告写一个视图引擎来调用第三方的模板引擎,我这里创建的是一个名为ViewEngine的类来调用第三方模板引擎,内容如下:

public class ViewEngine {
	public ViewEngine(ServletContext servletContext) {
        //创建第三方模板引擎,可以查看第三方模板引擎文档再编写
        //创建第三方模板引擎代码省略。。。。
	}
	public void render(ModelAndView mv, Writer writer){
		//将ModelAndView的Model数据和View视图路径交给第三方模板引擎去渲染,可以查看第三方模板引擎文档再编写
		//然后生成字节字节码,赋值给Writer对象
		//代码省略。。。。。
	}
}

因为ModelAndView其实就是将HTML模板(View)和数据模型(View)进行关联和渲染,需要HTML模板所以我们必须存储一个返回HTML模板的路径,数据模型可以使用键值对的方式存储任意类型数据,让所以可以定义为以下内容:

public class ModelAndView {
   private Map<String,Object> model;
   private String view;
   //省略其它代码。。。。
}

第三:管理和控制Controller,实际上处理网络请求的是Sevlet类,Controller类并不能直接接收到网络请求的,但是我们所有的网络请求路径都写在了Controller类的@GetMapping和@PostMapping中了,所有我们需要想办法的得到这些Controller类还有Controller类中的@GetMapping和@PostMapping中的请求路径以及@GetMapping和@PostMapping修饰的方法发的参数,再调用@GetMapping和@PostMapping修饰的方法,如何拿到以上说的信息呢?答案是反射,通过反射可以轻松拿到刚才说的所有信息。在Servlet类中通过反射来管理和控制Controller类,如何实现这个管理和控制Controller是MVC框架的重点,这些控制方法将会写在接管所有网络请求的ServletDispatcher中,其内容基本如下

@WebServlet("/")
public class ServletDispatcher extends HttpServlet {
   //存储请求路径String和请求处理类Dispatcher的映射,请求处理类Dispatcher类中主要存储的是反射信息,
   private final Map<String, Dispatcher> postRequestMappings=new HashMap<>();
   private final Map<String, Dispatcher> getRequestMappings=new HashMap<>();
   //所有的Controller类字节码
   private final Set<Class<?>> allControllers=new HashSet<>();
   //模板引擎,处理渲染HTML模板
   private ViewEngine viewEngine;
   //。。。。。。
   @Override
   public void init() {
       //初始化步骤如下
       //1.获取到所有的Controllerl类的字节码存储到allControllers类中,方便后续通过反射得到类中相关信息
       //2.通过allControllers类中字节码,获取类中相关信息,将请求路径String和请求处理类Dispatcher进行映射存储到postRequestMappings和getRequestMappings映射表中
       //3.初始化第三方模板引擎,方便后续直接调用渲染
       
   }
}

4.2.代码实现

4.2.1.创建MVC的相关注解

以下是@Controller、@GetMapping、@PostMapping的创建,

后面可以通过字节码的isAnnotationPresent()方法判断是否类或者方法方法上是否添加了相关类型的注解,通过字节码的getAnnotation()可以获取到注解并拿到注解中的参数

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {
}
@Retention(RUNTIME)
@Target(METHOD)
public @interface GetMapping {
   String value();
}
@Retention(RUNTIME)
@Target(METHOD)
public @interface PostMapping {
   String value();
}

4.2.2.bean实体(测试)

用于测试Controller类的返回ModelAndView

public class SignInBean {
    public String email;
    public String password;
}
public class User {
    public String email;
    public String password;
    public String name;
    public String description;
    
    public User() {
    }
    public User(String email, String password, String name, String description) {
        this.email = email;
        this.password = password;
        this.name = name;
        this.description = description;
    }
}

4.2.3.创建ModelAndView

public class ModelAndView {
   private Map<String,Object> model;
   private String view;
   public ModelAndView(String view) {
      this.view = view;
   }

   public ModelAndView(String view,String key,Object object) {
      HashMap<String, Object> stringObjectHashMap = new HashMap<>();
      stringObjectHashMap.put(key,object);
      this.model=stringObjectHashMap;
      this.view=view;
   }

   public ModelAndView(String view,Map<String, Object> model) {
      this.model = model;
      this.view = view;
   }

   public Map<String, Object> getModel() {
      return model;
   }

   public String getView() {
      return view;
   }
}

4.2.4.创建相关Controller类

@Controller
public class IndexController {

    @GetMapping("/")
    public ModelAndView index(HttpSession session) {
        User user = (User) session.getAttribute("user");
        return new ModelAndView("/index.html", "user", user);
    }

    @GetMapping("/hello")
    public ModelAndView hello(String name) {
        if (name == null) {
            name = "World";
        }
        return new ModelAndView("/hello.html", "name", name);
    }
}
@Controller
public class UserController {

    private Map<String, User> userDatabase = new HashMap<>() {
        {
            List<User> users = List.of( //
                    new User("bob@example.com", "bob123", "Bob", "This is bob."), new User("tom@example.com", "tomcat", "Tom", "This is tom."));
            users.forEach(user -> {
                put(user.email, user);
            });
        }
    };

    @GetMapping("/signin")
    public ModelAndView signin() {
        return new ModelAndView("/signin.html");
    }

    @PostMapping("/signin")
    public ModelAndView doSignin(SignInBean bean, HttpServletResponse response, HttpSession session) throws IOException {
        User user = userDatabase.get(bean.email);
        if (user == null || !user.password.equals(bean.password)) {
            response.setContentType("application/json");
            PrintWriter pw = response.getWriter();
            pw.write("{\"error\":\"Bad email or password\"}");
            pw.flush();
        } else {
            session.setAttribute("user", user);
            response.setContentType("application/json");
            PrintWriter pw = response.getWriter();
            pw.write("{\"result\":true}");
            pw.flush();
        }
        return null;
    }

    @GetMapping("/signout")
    public ModelAndView signout(HttpSession session) {
        session.removeAttribute("user");
        return new ModelAndView("redirect:/");
    }

    @GetMapping("/user/profile")
    public ModelAndView profile(HttpSession session) {
        User user = (User) session.getAttribute("user");
        if (user == null) {
            return new ModelAndView("/signin");
        }
        return new ModelAndView("/profile.html", "user", user);
    }
}

4.2.5.创建ViewEngine类

该类是用于调用第三方模板引擎将HTML视图模板和Model数据模型进行渲染,具体如何创建第三方模板引擎和如何通过第三方模板引擎渲染视图和数据可以参看第三方模板引擎的官方文档,我这里使用的thymeleaf模板引擎,其他模板引擎(FreeMarker、Velocity、pebbletemplates等)理论来说也一样。

public class ViewEngine {
   private final TemplateEngine TemplateEngine ;
   private final ServletContext servletContext;
   //通过构造方法创建thymeleaf模板引擎
   public ViewEngine(ServletContext servletContext) {
      this.servletContext=servletContext;
      JavaxServletWebApplication application = JavaxServletWebApplication.buildApplication(servletContext);
      // Templates will be resolved as application (ServletContext) resources
      final WebApplicationTemplateResolver templateResolver =
            new WebApplicationTemplateResolver(application);

      // HTML is the default mode, but we will set it anyway for better understanding of code
      templateResolver.setTemplateMode(TemplateMode.HTML);
      // html模板存放的位置
      templateResolver.setPrefix("/WEB-INF/templates/");
      templateResolver.setSuffix(".html");
      // Set template cache TTL to 1 hour. If not set, entries would live in cache until expelled by LRU
      templateResolver.setCacheTTLMs(Long.valueOf(3600000L));

      // Cache is set to true by default. Set to false if you want templates to
      // be automatically updated when modified.
      templateResolver.setCacheable(false);
      // 设置解析后生成的模板编码,不然中文乱码
      templateResolver.setCharacterEncoding("UTF-8");
      final TemplateEngine templateEngine = new TemplateEngine();
      templateEngine.setTemplateResolver(templateResolver);
      this.TemplateEngine= templateEngine;
   }
   //通过thymeleaf模板引擎对HTML视图模板和Model数据模型进行渲染
   public void render(ModelAndView mv, Writer writer) throws IOException {
      TemplateSpec templateSpec = new TemplateSpec(mv.getView(), (String) null);
      Context context = new Context(Locale.CHINA);
      context.setVariables(mv.getModel());
      this.TemplateEngine.process(templateSpec,context,writer);
   }
}

4.2.6.创建相关Dispatcher请求处理类

Dispatcher接口

所有请求处理类都将实现该接口

public interface Dispatcher {
   ModelAndView invoke(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

Post网络请求处理类

因为post请求一般用于json的传输,所以psot请求处理方法,参数只有HttpServletRequest、HttpServletResponse、HttpSession、Bean实体类 这4种类型,其中Bean实体类需要需要通过将传输过来的json数据转换为Bean实体类,这里用的是jackson工具将json转换为Bean实体类。

public class PostDispatcher implements Dispatcher {
   private Object instance; //存储通过反射创建的Controller类实例,后面通过反射调用方法会用
   private Method method; //存储通过反射获取Controller类的带@PostMapping注解的请求方法,后面会通过属性调用
   final ObjectMapper objectMapper; //该属性是jackson的对象映射,它会post提交的json映射为实体bean
   private Class<?>[] parameterTypes; //psot网络请求方法中参数的类型

   public PostDispatcher(Object instance, Method method, ObjectMapper objectMapper, Class<?>[] parameterTypes) {
      this.instance = instance;
      this.method = method;
      this.objectMapper = objectMapper;
      this.parameterTypes = parameterTypes;
   }

   public Object getInstance() {
      return instance;
   }

   public Method getMethod() {
      return method;
   }

   public ObjectMapper getParameterNames() {
      return objectMapper;
   }

   public Class<?>[] getParameterTypes() {
      return parameterTypes;
   }

   @Override
   public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response) throws Exception {
      //存储所有参数的对象实例,后面method.invoke调用方法会用到
      Object[] arguments = new Object[parameterTypes.length];
      //遍历带@PostMapping注解的方法参数的类型,将匹配的类型存储到arguments数组中
      for (int i = 0; i < parameterTypes.length; i++) {
         Class<?> parameterClass = parameterTypes[i];
         if (parameterClass == HttpServletRequest.class) { //方法参数有HttpServletRequest类型
            arguments[i] = request; //将HttpServletRequest对象存储到arguments数组中
         } else if (parameterClass == HttpServletResponse.class) {
            arguments[i] = response;
         } else if (parameterClass == HttpSession.class) {
            arguments[i] = request.getSession();
         } else {
            BufferedReader reader = request.getReader();  //获取post请求携带的json数据
            arguments[i] = this.objectMapper.readValue(reader, parameterClass);//使用jackson的对象映射方法将json数据映射为对应的Bean实体类
         }
      }
      return (ModelAndView) this.method.invoke(instance, arguments); //通过反射调用对应的psot网络处理方法
   }
}

get网络请求处理类

因为get请求一般用于基本数据类型的传输,他通过浏览器的地址栏输入参数然后传输,所以get请求处理方法,参数除了HttpServletRequest、HttpServletResponse、HttpSession、这3种类型外,还包含其他例如 int、String、long、boolean、double基本类型。

public class GetDispatcher implements Dispatcher {
   private Object instance;//存储通过反射创建的Controller类实例,后面通过反射调用方法会用
   private Method method;//存储通过反射获取Controller类的带@GetMapping注解的请求方法,后面会通过属性调用
   private String[] parameterNames; //get网络请求处理方法中参数的名字
   private Class<?>[] parameterTypes; //get网络请求处理方法中参数的类型

   public GetDispatcher(Object instance, Method method, String[] parameterNames, Class<?>[] parameterTypes) {
      this.instance = instance;
      this.method = method;
      this.parameterNames = parameterNames;
      this.parameterTypes = parameterTypes;
   }

   public Object getInstance() {
      return instance;
   }

   public Method getMethod() {
      return method;
   }

   public String[] getParameterNames() {
      return parameterNames;
   }

   public Class<?>[] getParameterTypes() {
      return parameterTypes;
   }

   @Override
   public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response) throws Exception {
      int length = parameterNames.length;
      //存储所有参数的对象实例,后面method.invoke调用方法会用到
      Object[] arguments = new Object[length];
      for (int i=0;i<length;i++){
         String parameterName = parameterNames[i];
         Class<?> parameterClass = parameterTypes[i];
         if (parameterClass == HttpServletRequest.class) { //方法参数有HttpServletRequest类型
            arguments[i] = request //将HttpServletRequest对象存储到arguments数组中
         } else if (parameterClass == HttpServletResponse.class) {
            arguments[i] = response;
         } else if (parameterClass == HttpSession.class) {
            arguments[i] = request.getSession();
         } else if (parameterClass == int.class) { //方法参数有int基本类型
             //将int基本类型的包装类对象存储到arguments数组中
            arguments[i] = Integer.valueOf(getOrDefault(request, parameterName, "0")); 
         } else if (parameterClass == long.class) {
            arguments[i] = Long.valueOf(getOrDefault(request, parameterName, "0"));
         } else if (parameterClass == boolean.class) {
            arguments[i] = Boolean.valueOf(getOrDefault(request, parameterName, "false"));
         } else if (parameterClass == double.class) {
            arguments[i] = Double.valueOf(getOrDefault(request, parameterName, "false"));
         } else if (parameterClass == String.class) {
            arguments[i] = getOrDefault(request, parameterName, "");
         } else {
            throw new RuntimeException("Missing handler for type: " + parameterClass);
         }
      }
      //通过反射调用对应的get网络处理方法
      return (ModelAndView) this.method.invoke(this.instance, arguments);
   }
   //如果传入的基本类型没有赋值时设定一个默认值
   private String getOrDefault(HttpServletRequest request, String name, String defaultValue) {
      String s = request.getParameter(name);
      return s == null ? defaultValue : s;
   }
}

4.2.7.创建FileServlet静态资源处理

该类用于返回/static路径下的静态资源例如图片、js文件、css文件等

@WebServlet(urlPatterns = { "/favicon.ico", "/static/*" })
public class FileServlet extends HttpServlet {
   protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
      ServletContext ctx = req.getServletContext();
      // RequestURI包含ContextPath,需要去掉:
      String urlPath = req.getRequestURI().substring(ctx.getContextPath().length());
      // 获取真实文件路径:
      String filepath = ctx.getRealPath(urlPath);
      if (filepath == null) {
         // 无法获取到路径:
         resp.sendError(HttpServletResponse.SC_NOT_FOUND);
         return;
      }
      Path path = Paths.get(filepath);
      if (!path.toFile().isFile()) {
         // 文件不存在:
         resp.sendError(HttpServletResponse.SC_NOT_FOUND);
         return;
      }
      // 根据文件名猜测Content-Type:
      String mime = Files.probeContentType(path);
      if (mime == null) {
         mime = "application/octet-stream";
      }
      resp.setContentType(mime);
      // 读取文件并写入Response:
      OutputStream output = resp.getOutputStream();
      try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
         input.transferTo(output);
      }
      output.flush();
   }
}

4.2.8.创建MVC核心处理类ServletDispatcher

ServletDispatcher类通过反射来管理和控制Controller类,是整个MVC框架的重点,特别是初始化方法的实现。

@WebServlet("/") //接管所有的网络请求,是个sevlet类,因为只有sevlet类可以接收到网络请求
public class ServletDispatcher extends HttpServlet {
   //存储请求路径String和请求处理类Dispatcher的映射,请求处理类Dispatcher类中主要存储的是反射信息,
   private final Map<String, Dispatcher> postRequestMappings=new HashMap<>();
   private final Map<String, Dispatcher> getRequestMappings=new HashMap<>();
   //所有的Controller类字节码
   private final Set<Class<?>> allControllers=new HashSet<>();
   //模板引擎,处理渲染HTML模板
   private ViewEngine viewEngine;

   @Override
   public void init() { //初始化方法,当该类被调用创建时会调用一次,后面不在调用
      System.out.println("初始化");
     
      try {
         //1)获取所有的Controller字节码(包扫描),存储到allControllers中
         scannerPackage();
      } catch (Exception e) {
         e.printStackTrace();
      }
      
      try {
         //2)将请求路径url和Dispatcher请求处理类实例键值映射并存入Map
         handleRequestMappings();
      } catch (Exception e) {
         e.printStackTrace();
      }
      //3)初始化视图引擎,传入ServletContext上下文,sevlet类可以直接获取到
      this.viewEngine=new ViewEngine(getServletContext());
   }
   //1)获取所有的Controller字节码(包扫描),存储到allControllers中
   public void scannerPackage() throws Exception{
      String packagePath="com.web.mvc.controller";
      scannerPackage(packagePath);
   }
   public void scannerPackage(String packagePath) throws Exception {
      String filePath=packagePath.replace(".", File.separatorChar+"");
      URL resourcePath = Thread.currentThread().getContextClassLoader().getResource(filePath);
      if (resourcePath==null){
         throw new IOException("包路径未找到:"+packagePath);
      }
      File file = new File(resourcePath.getFile()+"");
      //得到com.web.mvc.controller包中的所有文件(类和文件夹)
      File[] files = file.listFiles();
      assert files != null;
	  //遍历所有的文件,并得到controller类的字节码,存储到allControllers中
      listFiles(files,packagePath);
   }
   private void listFiles(File[] files,String packagePath) throws Exception {
      for (File item : files){
         if (item.isFile()&&item.getName().endsWith(".class")){
            String tempPath=packagePath+"."+
                  item.getName().replace(".class","");
             //到controller类的字节码并存储到allControllers中
            this.allControllers.add(Class.forName(tempPath));
         }else if (!item.isFile()){
            listFiles(Objects.requireNonNull(item.listFiles()),packagePath+"."+item.getName());
         }
      }
   }
   //2)将请求路径url和Dispatcher请求处理类实例键值映射并存入Map
   private void handleRequestMappings() throws Exception {
       //遍历所有的controller字节码
      for (Class<?> item:this.allControllers){
         //检查该controller字节码类上是否添加上了@Controller注解
         if (!item.isAnnotationPresent(Controller.class)){
            continue;
         }
         //通过controller类的字节码用反射实例化controller对象
         Object controllerItem = item.getConstructor().newInstance();
         //controller类的字节码得到所有controller类中的方法
         Method[] methods = item.getMethods();
         for (Method method : methods) { //遍历controller类中的方法
             //判断controller类中的方法是否添加@GetMapping注解
             if (method.isAnnotationPresent(GetMapping.class)){ 
                GetMapping annotation = method.getAnnotation(GetMapping.class); //得到@GetMapping注解
                String value = annotation.value(); //得到@GetMapping注解的参数,也就是网络请求路径
                //通过stream流将get网络请求处理方法中的所有参数名称转换为String类型的数组
                String[] parameterNames
                      = Arrays.stream(method.getParameters()).map(temp -> temp.getName()).toArray(String[]::new);
                //创建get网络请求处理类对象实例,
                //controllerItem是反射创建的controller实例,
                //method是controller实例中的get网络请求处理方法
                //parameterNames是get网络请求处理方法中的所有参数名
                //method.getParameterTypes()是get网络请求处理方法中的所有参数的类型
                GetDispatcher getDispatcher
                      = new GetDispatcher(controllerItem, method, parameterNames, method.getParameterTypes());
                 //将get网络请求路径url和GetDispatcher网络请求处理类实例键值映射并存入getRequestMappings
                 getRequestMappings.put(value,getDispatcher);
                 //判断controller类中的方法是否添加@GetMapping注解
             }else if (method.isAnnotationPresent(PostMapping.class)){
                PostMapping annotation = method.getAnnotation(PostMapping.class);//得到@PostMapping注解
                String value = annotation.value();//得到@PostMapping注解中的参数值
                ObjectMapper objectMapper = new ObjectMapper(); //jackson工具中的类对象用于将Json数据转为Bean实体
                //创建psot网络请求处理类对象实例,
                //controllerItem是反射创建的controller实例,
                //method是controller实例中的psot网络请求处理方法
                //objectMapper是jackson工具中的类对象用于将Json数据转为Bean实体
                //method.getParameterTypes()是post网络请求处理方法中的所有参数的类型
                PostDispatcher postDispatcher
                      = new PostDispatcher(controllerItem, method, objectMapper, method.getParameterTypes());
                 //将psot网络请求路径url和PostDispatcher网络请求处理类实例键值映射并存入postRequestMappings
                 postRequestMappings.put(value,postDispatcher);
             }
         }
      }
   }
    //MVC核心方法,该方法用于控制和调用controller类
   	private void process(HttpServletRequest req,
	                     HttpServletResponse resp,
	                     Map<String, ? extends Dispatcher> requestMappings) throws Exception {
		//得到网络请求中的请求路径
         String requestURI = req.getRequestURI();
         //通过网络请求路径得到网络请求处理类(GetDispatcher或者PostDispatcher)对象
		Dispatcher dispatcher = requestMappings.get(requestURI);
		if (dispatcher == null) {
			resp.sendError(404);
			return;
		}
		ModelAndView mv = null;
         //调用网络请求处理类(GetDispatcher或者PostDispatcher)对象中controller实例的post或者get网络请求处理方法得到ModelAndView,这里比较绕,可以多看下
		ModelAndView invoke = dispatcher.invoke(req, resp);
		if (invoke==null){ //处理psot网络请求处理方法中返回的null值
			return;
		}
		mv=invoke;
         //通过HttpServletResponse对象设置响应http的header信息
		resp.setContentType("text/html");
		resp.setCharacterEncoding("UTF-8");
         //通过HttpServletResponse对象设置响应http的body信息
		PrintWriter pw = resp.getWriter();//获取字节码
		this.viewEngine.render(mv, pw);//调用模板引擎渲染html页面,并作为http的body响应体
		//刷新缓存,将http信息响应给浏览器
         pw.flush();
	}
   //sevlet类中继承的方法,所有的get请求将调用
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
      try {
         //调用MVC核心方法,该方法用于控制和调用controller类,注意这里传入的是getRequestMappings
         process(req,resp,this.getRequestMappings);
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
   //sevlet类中继承的方法,所有的post请求将调用
   @Override
   protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
      try {
         //调用MVC核心方法,该方法用于控制和调用controller类,注意这里传入的是postRequestMappings
         process(req,resp,this.postRequestMappings);
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

4.2.9.创建html模板

我这里用的thymeleaf模板引擎,具体语法可以查看thymeleaf官方文档

image-20250418234201254

base.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" th:fragment="html (head,content)">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>MVC</title>
    <script src="/static/js/jquery.js"></script>
    <link href="/static/css/bootstrap.css" rel="stylesheet">
  </head>
  <body style="font-size:16px" >
    <div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm">
      <h5 class="my-0 mr-md-auto font-weight-normal"><a href="/">Home</a></h5>
      <nav class="my-2 mr-md-3">
        <a class="p-2 text-dark" target="_blank" href="https://www.leduo.com/">Learn</a>
        <a class="p-2 text-dark" target="_blank" href="#">Source</a>
      </nav>
      <th:block th:if="${user==null}">
        <a href="/signin" class="btn btn-outline-primary">Sign In</a>
      </th:block>
      <th:block th:if="${user!=null}">
        <span>Welcome, <a href="/user/profile">[[${user.name}]]</a></span>
        &nbsp;&nbsp;&nbsp;
        <a href="/signout" class="btn btn-outline-primary">Sign Out</a>
      </th:block>
    </div>

    <div class="container" style="max-width: 960px">
      <div class="row">
        <div class="col-12 col-md">
          <section>
            <th:block th:replace="${content}"/>
          </section>
        </div>
      </div>

      <footer class="pt-4 my-md-5 pt-md-5 border-top">
        <div class="row">
          <div class="col-12 col-md">
            <h5>Copyright&copy;2020</h5>
            <p>
              <a target="_blank" href="https://www.leduo.work/">Learn</a> -
              <a target="_blank" href="https://www.leduo.work/">Download</a> -
              <a target="_blank" href="https://www.leduo.work/">Github</a>
            </p>
          </div>
        </div>
      </footer>
    </div>
  </body>
</html>

hello.html

<div th:replace="~{base :: html(head = null,content = ~{::content})}">
    <th:block>
        <h3>hello [[${name}]]!</h3>
    </th:block>
</div>

index.html

<div th:replace="~{base :: html(head = null,content = ~{::content})}">
    <th:block th:fragment="content">
        <th:block th:if="${user!=null}">
            <h3>Welcome [[${user.name}]]!</h3>
        </th:block>
        <th:block th:if="${!user!=null}">
            <h3>Welcome</h3>
        </th:block>
    </th:block>
</div>

profile.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{base::html (head=null,content=~{::Profile})}">

<th:block th:fragment="Profile">
    <h3>Profile</h3>
    <p>Email: [[${ user.email }]]</p>
    <p>Name: [[${user.name}]]</p>
    <p>Description: [[${user.description}]]</p>
</th:block>
</html>

signin.html

<th:block th:replace="~{base::html (head=null,content=~{::#signinForm})}">
  <div>
    <form id="signinForm">
      <div class="form-group">
        <p id="error" class="text-danger"></p>
      </div>
      <div class="form-group">
        <label>Email</label>
        <input id="email" type="email" class="form-control" placeholder="Email">
      </div>
      <div class="form-group">
        <label>Password</label>
        <input id="password" type="password" class="form-control" placeholder="Password">
      </div>
      <button type="submit" class="btn btn-outline-primary">Submit</button>
    </form>
  </div>
</th:block>
<script>
      console.log("sasasasaassa")
      $(function () {
        console.log("sasasasaassa")
        $('#signinForm').submit(function (e) {
          e.preventDefault();
          var data = {
            email: $('#email').val(),
            password: $('#password').val()
          };
          $.ajax({
            type: 'POST',
            url: '/signin',
            data: JSON.stringify(data),
            success: function (resp) {
              if (resp.error) {
                $('#error').text(resp.error);
              } else {
                location.assign('/');
              }
            },
            contentType: 'application/json',
            dataType: 'json'
          });
        });
      });
    </script>

4.2.10.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.web.mvc</groupId>
    <artifactId>webmvc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>web-mvc</name>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.target>11</maven.compiler.target>
        <maven.compiler.source>11</maven.compiler.source>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf</artifactId>
            <version>3.1.3.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!--war打包插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.1</version>
            </plugin>
        </plugins>
    </build>
</project>

4.2.11.总结

管理和控制Controller是MVC的核心,而MVC的核心的核心是通过反射获取Controller类实现控制和调用。

github源码地址:https://github.com/AllenWarg/servlet-mvc

gieet源码地址:https://gitee.com/cgh-yfx/servlet-mvc

运行结果如下:

image-20250419003241137


Comment