Java Servlet 基础

目录结构

使用Idea创建一个servlet项目目录结构如下(Tomcat 10 + Servlet 5 + Jdk 21)

image-20240117164807689

其中:

Main/java 目录存放java源码

resources 目录存放资源文件,随war/jar一起打包

webapp 目录相当于服务器的www目录,客户端可直接访问

webapp/WEB-INF 目录为安全目录,客户端无法直接访问,一版存放web.xml以及class和lib文件

对应版本

需要注意Servlet与Tomcat的版本需要对应,对应表如下

image-20240117204903314

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处理后的请求

image-20240226154910448

Container包含四个容器:Engine、Host、Context、Wrapper,其中Engine可以看作是容器对外提供的功能入口,用于管理各个Host,Host就是每个虚拟主机,区别在域名上,Context就是一个Web应用,Wrapper就是具体到的每个Servlet,用于管理Servlet

四者简单理解图

image-20240226155717728

三种Context

ServletContext:是一个接口,保存Web应用中所有的Servlet上下文,定义了很多对于上下文的操作(自己看)

ApplicationContext:ServletContext的具体实现,实现了ServletContext接口定义的一些方法。ApplicationContext采用Facade设计模式

image-20240304154846425

StandardContext:StandardContext是Context的标准实现类,也就是用的就是它,ApplicationContext类中各种操作实际上都是调用StandardContext,总体来说最终交互的Context就是它,这也是之后获取Context都要获取它的原因

关系图总结

image-20240304163110309

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的方法

  1. 获取顶级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();
  1. 获取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类型

image-20240322165914316

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() {
    }
}

其中:

  1. public class IndexServlet extends HttpServlet

Servlet总是继承自HttpServlet抽象类,然后重写doGet,doPost等方法(根据需求)来处理对应的http方法请求,所有Servlet支持的http方法如下(do开头

image-20240117212326634
  1. @WebServlet(name = "indexServlet", value= "/")

注解,用于标注Servlet,name用于表示Servlet的名字,可有可无,value用于指定路由

也可以使用urlPatterns这样表示@WebServlet(urlPatterns = "/")

  1. 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│ │
                     │            └────────┘ │
                     └───────────────────────┘

关于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() {
    }
}
image-20240118145628756

关于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

借用图,大概就是这样的结构

image-20240118155120164

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无法排序,主要是根据类名

image-20240118155031997

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页面

0%