目录结构
使用Idea创建一个servlet项目目录结构如下(Tomcat 10 + Servlet 5 + Jdk 21)
其中:
Main/java 目录存放java源码
resources 目录存放资源文件,随war/jar一起打包
webapp 目录相当于服务器的www目录,客户端可直接访问
webapp/WEB-INF 目录为安全目录,客户端无法直接访问,一版存放web.xml以及class和lib文件
对应版本
需要注意Servlet与Tomcat的版本需要对应,对应表如下
Servlet API
以下涉及到的测试分析均以 Tomcat 为例
Servlet 3.0加入了动态注册。我们通过xml配置文件还是注解配置的Servlet、Filter、Listener都需要ServletContext去加载,动态注册也就是说可以在初始化ServletContext时使用add/create运行时动态添加
Tomcat相关
先了解一些Tomcat相关概念
下图为Tomcat处理请求逻辑结构图,简单来说,Tomcat就是Web服务器和Servlet容器的结合,Connector用于接受并解析请求,Container处理Connector解析后的请求。一个Tomcat Server包含多个Service,每个Service独立存在,共享一个JVM和系统库,每个Service中又包含多个Connector和一个Container,Connector用于处理客户端发来的请求,不同的Connector处理不同类型的请求协议,Container处理Connector处理后的请求
Container包含四个容器:Engine、Host、Context、Wrapper,其中Engine可以看作是容器对外提供的功能入口,用于管理各个Host,Host就是每个虚拟主机,区别在域名上,Context就是一个Web应用,Wrapper就是具体到的每个Servlet,用于管理Servlet
四者简单理解图
三种Context
ServletContext:是一个接口,保存Web应用中所有的Servlet上下文,定义了很多对于上下文的操作(自己看)
ApplicationContext:ServletContext的具体实现,实现了ServletContext接口定义的一些方法。ApplicationContext采用Facade设计模式
StandardContext:StandardContext是Context的标准实现类,也就是用的就是它,ApplicationContext类中各种操作实际上都是调用StandardContext,总体来说最终交互的Context就是它,这也是之后获取Context都要获取它的原因
关系图总结
Filter Listener Servlet加载顺序:Listener -> Filter -> Servlet
StandardContext#startInternal()
中调用顺序(其余省略),可以看到调用顺序为listenerStart() -> filterStart() -> loadOnStartup()
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
|
...
// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
...
// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
...
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())) {
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
...
|
StandardContext的获取
StandardContext在Tomcat类型的内存马利用中极其重要,获取StandardContext的方法有很多,这边讲两种jsp中获取StandardContext的方法
- 获取顶级request
1
2
3
4
|
Field requestField = request.getClass().getDeclareField("request");
requestField.serAccessible(true);
Request req = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
|
- 获取ServletContext然后向下获取 -> ApplicationContext -> StandardContext
1
2
3
4
5
6
7
|
ServletContext servletContext = request.getSession().getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
|
既然聊到jsp那就谈一谈,jsp本质就是servlet,如下是jsp文件被Tomcat转换后的java文件片段,我们写的代码在_jspService方法中在jsp中拿到的request为jakarta.servlet.http.HTtpServletRequest类型
Servlet
Servlet运行在Servlet容器上,Servlet容器就是Servlet代码的运行环境,当Servlet请求到达时,容器会根据web.xml或注解决定使用哪个类
生命周期(Tomcat)
- 加载:当 Tomcat 第一次访问Servlet时,Tomcat 会负责创建Servlet实例
- 初始化:当 Servlet 被初始化后,Tomcat 会调用 init() 方法初始化这个对象
- 处理服务:当浏览器发送请求访问Servlet时,Servlet 会调用 service() 方法处理请求
- 销毁:当 Tomcat 关闭时或检测到 Servlet 要从 Tomcat 删除的时候会自动调用 destroy() 方法,让该实例释放掉所占的资源。长时间不使用的 Servlet 也会被自动销毁
- 卸载:当 Servlet 调用完 destroy() 方法后,等待垃圾回收。如果有需要再使用这个 Servlet,会重新调用 init() 方法进行初始化操作
一个基础的Servlet结构如下
IndexServlet.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package org.example.servlettest;
import java.io.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
@WebServlet(name = "indexServlet", value = "/")
public class IndexServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
PrintWriter pw = response.getWriter();
pw.write("<h1>Hello!!!</h1>");
pw.flush();
}
public void destroy() {
}
}
|
其中:
public class IndexServlet extends HttpServlet
Servlet总是继承自HttpServlet抽象类,然后重写doGet,doPost等方法(根据需求)来处理对应的http方法请求,所有Servlet支持的http方法如下(do开头
@WebServlet(name = "indexServlet", value= "/")
注解,用于标注Servlet,name用于表示Servlet的名字,可有可无,value用于指定路由
也可以使用urlPatterns这样表示@WebServlet(urlPatterns = "/")
,
HttpServletRequest request, HttpServletResponse response
传入的HTtpServletRequest和HttpServletResponse对象分别代表Http请求和相应,类中封装好了对应的处理http数据的方法
重定向与转发
重定向是客户端的重定向,http返回301或302请求并制定location跳转
1
2
|
// 重定向简单实现
response.sendRedirect("https://baidu.com");
|
画个图简单理解
1
2
3
4
5
6
7
8
9
10
11
|
Get
┌───────┐───────────>┌───────┐
│Browser│ │Servlet│
└───────┘<───────────└───────┘
302
Get
┌───────┐───────────>┌───────────────┐
│Browser│ │location server│
└───────┘<───────────└───────────────┘
200
|
转发是服务端Servlet内部行为,客户端方面感受不到路由的变化
1
2
|
// 转发简单实现
request.getRequestDispatcher("/test").forward(request, response);
|
上面代码的意思是,当前Servlet收到请求后,自己并不作出响应,而是将请求和相应都交个/test这个路由的Servlet去处理
画个图简单理解
1
2
3
4
5
6
7
8
9
10
11
|
┌───────────────────────┐
│ ┌────────┐ │
│ ────────>│Servlet1│ │
Get │ └────────┘ │
┌───────┐───────────>│ │ │
│Browser│ │ │ │
└───────┘<───────────│ V │
302 │ ┌────────┐ │
│ ────────>│Servlet2│ │
│ └────────┘ │
└───────────────────────┘
|
Session and Cookie
关于Servlet 的 Session:
Servlet中使用HttpSession管理session,session ID 由客户端cookie中JSESSIONID值来确定,简单理解就是服务器中自动维护了一个ID到HttpSession的映射表,服务器使用名为JSESSIONID的Cookie来识别Session,JSESSIONID由Servlet容器自动创建;所有用户的Session都存储在内存中
Demo
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
|
package org.example.servlettest;
import java.io.*;
import java.util.Map;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
@WebServlet(name = "helloServlet", value = "/hello")
public class HelloServlet extends HttpServlet {
private Map<String, String> users = Map.of("fucker", "123", "sucker", "123");
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
PrintWriter pw = response.getWriter();
if (username != null && password != null && users.get(username) != null && users.get(username).equals(password)) {
request.getSession().setAttribute("user", username);
pw.write("<h1>Hello " + username + "</h1>");
} else {
pw.write("<h1>Hello mother fucker</h1>");
}
pw.flush();
}
public void destroy() {
}
}
|
关于Servlet的Cookie:
JSESSIONID就是一个cookie,一般情况下除了这个设置任何名字cookie都可以
不同于session,因为cookie并不是servlet专属所以想要获取指定cookie需要便利一遍
Demo
IndexServlet.java
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
|
package org.example.servlettest;
import java.io.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
@WebServlet(name = "indexServlet", value = "/")
public class IndexServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
PrintWriter pw = response.getWriter();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("isLogin") && cookie.getValue().equals("1")) {
pw.write("<h1>Hello login user</h1>");
}
}
}
pw.write("<h1>Fuck off!!! no login user</h1>");
pw.flush();
}
public void destroy() {
}
}
|
HelloServlet.java
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
|
package org.example.servlettest;
import java.io.*;
import java.util.Map;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
@WebServlet(name = "helloServlet", value = "/hello")
public class HelloServlet extends HttpServlet {
private Map<String, String> users = Map.of("fucker", "123", "sucker", "123");
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
PrintWriter pw = response.getWriter();
if (username != null && password != null && users.get(username) != null && users.get(username).equals(password)) {
Cookie cookie = new Cookie("isLogin", "1");
response.addCookie(cookie);
pw.write("<h1>Hello " + username + "</h1>");
} else {
pw.write("<h1>Login you mother fucker</h1>");
}
pw.flush();
}
public void destroy() {
}
}
|
Filter
简单理解就是客户端与Servlet之间的过滤,处理发给Servlet的Request,修改Servlet返回的Response
使用@WebFilter注解标注filter,urlPatterns设置路由匹配规则,必须实现Filter接口,并且实现doFilter方法,jdk1.8需要手动重写init destroy方法,高版本不需要
一般处理结束后使用FilterChain.doFilter(request, response)将传递给下一个Filter或者Servlet
借用图,大概就是这样的结构
Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package org.example.servlettest;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
public class TestFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
System.out.println("Before filter");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("After filter");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}
|
FilterChain
顾名思义就是几个Filter连成的链,一个一个过,由web.xml配置时执行顺序由<filter-mapping>指定,由注解配置的Filter无法排序,主要是根据类名
Listener
监听器,需要被监听对象发生事件时触发,事件主要是指方法调用、属性改变等
一个事件一般仅由一个监听器处理
根据实现的Listener接口,决定监听何种事件
ServletContextListener:整个web应用程序初始化完成后,以及web应用程序关闭后触发
HttpSessionListener:监听HttpSession的创建和销毁事件
ServletRequestListener:监听ServletRequest请求的穿件和销毁事件
ServletRequestAttributeListener:监听ServletRequest请求的属性变化事件
ServletContextAttributeListener:监听ServletContext的属性变化事件
Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package org.example.servlettest;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
@WebListener
public class TestListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
System.out.println("Initialized");
}
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("Destroyed");
}
}
|
JSP
Java Server Pages,简单理解就是内嵌java代码的html页面