Servlet

Servlet在Java Web中的定位

Servlet = Server + Applet,可以理解为“运行在服务器端的小程序”。

更准确地说:

  • Servlet 是一个 Java 类
  • 它不是独立运行的主程序
  • 它必须运行在 Web 容器中,例如 Tomcat
  • 它的任务是接收 HTTP 请求、处理业务、返回 HTTP 响应
浏览器 -> HTTP请求 -> Tomcat -> Servlet -> 业务处理 -> HTTP响应 -> 浏览器

Servlet 的核心价值就在于:它让服务器能“根据请求动态做事”,而不只是返回文件。

核心概念:Servlet 是运行在 Web 容器中的 Java 类,用来处理 HTTP 请求并生成动态响应。

Web应用目录与运行方式

一个典型 Web 应用的标准结构如下:

WebRoot/
├── html、css、js、images 等静态资源
├── META-INF/
└── WEB-INF/
    ├── classes/
    ├── lib/
    └── web.xml

其中最重要的是 WEB-INF/

  • 浏览器不能直接访问 WEB-INF 下的内容
  • 编译后的 .class 文件通常放在 WEB-INF/classes
  • 依赖 jar 包通常放在 WEB-INF/lib
  • web.xml 是传统 Web 应用的配置文件

实际开发中,我们更常见的是 Maven Web 工程:

project/
├── pom.xml
└── src/
    ├── main/
    │   ├── java/
    │   ├── resources/
    │   └── webapp/
    │       ├── WEB-INF/
    │       └── 静态资源
    └── test/

它与标准 Web 结构的对应关系如下:

Maven 目录部署后位置说明
src/main/javaWEB-INF/classesJava 源码编译后输出到这里
src/main/resourcesWEB-INF/classes配置文件等资源会被复制到这里
src/main/webappWeb 根目录HTML、CSS、JS、图片等会直接发布
pom.xml 中依赖WEB-INF/lib依赖 jar 会被放到应用运行时目录

使用war打包之后的java项目结构

开发和编译的路径对应:

IDEA 配合 Tomcat 运行 Web 项目时,本质上做了三件事:

  1. 编译 Java 源码
  2. 按 Web 应用结构组装输出目录
  3. 把输出目录映射给 Tomcat 访问

常见配置步骤如下:

  1. Run -> Edit Configurations
  2. 添加 Tomcat Server -> Local
  3. 配置 Tomcat Home
  4. 在 Deployment 中添加 war exploded
  5. 设置 Application Context

图形化界面完成虚拟映射

映射文件存放在临时文件夹中,为了避免污染原本的tomcat配置

其中 Application Context 决定访问路径前缀。

例如:

Application Context = /demo

那么访问地址通常就是:

http://localhost:8080/demo/hello

这里的 URL 并不是“随便写的字符串”,它和 Tomcat 中的应用路径、资源路径是对应起来的。

回到 Tomcat 服务器本身:

  • webapps 下的目录天然会形成可访问应用
  • ROOT 对应访问 /
  • 其他目录名通常对应自己的上下文路径
  • 通过 conf/Catalina/localhost/*.xml 配置 docBase 时,本质上是在把某个访问路径映射到指定磁盘目录

Servlet 相关的很多 404,本质上都不是“Servlet 写错了”,而是部署结构不对

常见错误包括:

  • 静态资源放错目录,导致浏览器找不到
  • Application Context 配错,访问地址少了或多了前缀
  • 类没有编译进 WEB-INF/classes
  • 依赖没打进去,导致启动失败后误以为是 404

依赖和请求发放

引入 Servlet API,在 Maven 中,Servlet API 通常这样配置:

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

这里 scope 必须是 provided,原因是:

  • 编译时需要这个依赖
  • 运行时 Tomcat 已经自带 Servlet API(lib目录下的servlet.jar)
  • 如果再把它打进项目里,容易发生版本冲突

Servlet 的三种开发方式

方式说明是否推荐
实现 Servlet 接口最底层,方法最多不推荐作为入门写法
继承 GenericServlet屏蔽部分模板代码一般了解即可
继承 HttpServlet适合 HTTP 请求处理推荐

在 Web 开发里,绝大多数情况下都应该直接继承 HttpServlet

示例:

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/plain;charset=UTF-8");
        resp.getWriter().write("Hello Servlet");
    }
}

访问:

http://localhost:8080/应用上下文/hello

如果页面能看到 Hello Servlet,说明整个链路已经通了。

请求分发流程

根据请求报文中的请求行中的请求方法的不同,执行不同的doXXX方法,Servlet的核心开发就是重写父类中的doXXX方法

Servlet被多线程访问,所以需要保证线程安全:

 GET 与 POST 的基本分工

方法常见用途特点
GET查询、访问页面参数通常跟在 URL 后
POST提交表单、提交数据参数通常在请求体中

一个常见写法是:

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

这表示“先统一到同一套处理逻辑”。 但要注意:只有当 GET 和 POST 的业务语义确实一致时,才适合这样写。

@WebServlet 与 URL 映射规则

@WebServlet 常用属性

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebServlet {
    String name() default "";
    String[] value() default {};
    String[] urlPatterns() default {};
    int loadOnStartup() default -1;
    WebInitParam[] initParams() default {};
}

最常用的几个属性是:

属性作用
value指定 URL 映射路径
urlPatterns与 value 等价
loadOnStartup指定启动时是否提前加载
initParams指定 Servlet 的初始化参数

value 和 urlPatterns常见写法

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {}

当只写一个 value 属性时,可以省略 value =。以下写法也成立:

@WebServlet(value = "/hello", loadOnStartup = 1)
public class HelloServlet extends HttpServlet {}

多个路径映射到同一个 Servlet 也可以:

@WebServlet({"/hello", "/hi", "/greeting"})
public class HelloServlet extends HttpServlet {}

URL-pattern 四种常见形式

类型示例说明
精确匹配/hello只匹配指定路径
路径匹配/user/*匹配这一前缀下的路径
扩展名匹配*.do匹配指定扩展名
缺省匹配/匹配没有被其他规则处理的请求

匹配优先级遵循:精确匹配 > 路径匹配 > 扩展名匹配 > 缺省匹配

 缺省 Servlet 与静态资源

Tomcat 内部有一个默认 Servlet,专门处理静态资源。

这意味着:

  • 浏览器访问图片、CSS、JS 等静态资源时,通常不是你自己写的 Servlet 在处理
  • 如果你自己写了 @WebServlet("/"),就可能把默认静态资源处理逻辑“截胡”

示例:

@WebServlet("/")
public class MyDefaultServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/html;charset=UTF-8");
        resp.getWriter().write("<h1>自定义缺省处理</h1>");
    }
}

这类写法在框架内部有它的用途,但对于入门阶段来说,要先知道风险:

  • 静态资源可能访问不到
  • 所有未命中的请求都会落到这个 Servlet

Servlet 生命周期与单例多线程模型

Servlet 的生命周期由容器管理,不由我们手动控制。可以用这条链路理解:

加载类 -> 创建实例 -> init() -> service() 多次调用 -> destroy()

各个方法的执行时机

方法执行次数谁调用典型用途
构造方法1 次容器创建对象
init()1 次容器初始化资源
service()多次容器分发请求
destroy()1 次容器释放资源

生命周期观察示例

@WebServlet(value = "/life", loadOnStartup = 1)
public class LifeServlet extends HttpServlet {

    public LifeServlet() {
        System.out.println("1. 构造方法执行");
    }

    @Override
    public void init() throws ServletException {
        System.out.println("2. init() 执行");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        System.out.println("3. doGet() 执行");
        resp.getWriter().write("ok");
    }

    @Override
    public void destroy() {
        System.out.println("4. destroy() 执行");
    }
}

日志中可以观察到:

  • Tomcat 启动时,若配置了 loadOnStartup,会先执行构造和 init()
  • 每访问一次 /life,会执行一次 doGet()
  • 正常关闭容器时,才会执行 destroy()

loadOnStartup 的意义

loadOnStartup 默认是 -1,表示第一次访问时再初始化。

@WebServlet(value = "/demo", loadOnStartup = 1)
public class DemoServlet extends HttpServlet {}

当值大于等于 0 时:

  • Tomcat 启动阶段就会初始化这个 Servlet
  • 数字越小,优先级通常越高

适用场景:

  • 需要预加载配置
  • 需要启动时初始化缓存
  • 希望第一次访问时响应更快

为什么说 Servlet 默认是“单例多线程”

容器通常只创建一个 Servlet 实例,但会用多个线程处理不同请求。

这就意味着:

  • 局部变量一般是线程安全的
  • 成员变量可能被多个请求共享
  • 把“请求相关状态”放进成员变量是很危险的

错误示例:

@WebServlet("/counter")
public class CounterServlet extends HttpServlet {

    private int count = 0;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        count++;
        resp.getWriter().write("count = " + count);
    }
}

问题在于:多个请求同时进来时,count++ 不是线程安全操作。

更安全的写法:

import java.util.concurrent.atomic.AtomicInteger;

@WebServlet("/counter")
public class CounterServlet extends HttpServlet {

    private final AtomicInteger count = new AtomicInteger(0);

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        int current = count.incrementAndGet();
        resp.getWriter().write("count = " + current);
    }
}

ServletConfig 与 ServletContext

主要使用Context

对象作用域典型用途
ServletConfig单个 Servlet读取当前 Servlet 的初始化参数
ServletContext整个 Web 应用共享全局数据、读取应用级信息

ServletConfig 示例

@WebServlet(
    value = "/config",
    initParams = {
        @WebInitParam(name = "username", value = "root"),
        @WebInitParam(name = "password", value = "123456")
    }
)
public class ConfigServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        ServletConfig config = getServletConfig();

        String username = config.getInitParameter("username");
        String password = config.getInitParameter("password");

        resp.setContentType("text/plain;charset=UTF-8");
        resp.getWriter().write(username + " / " + password);
    }
}

这一组参数只服务于当前这个 Servlet,不会自动共享给其他 Servlet。

ServletContext 示例

@WebServlet("/put")
public class PutServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        ServletContext context = getServletContext();
        context.setAttribute("appName", "ServletDemo");
    }
}

@WebServlet("/get")
public class GetServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        ServletContext context = getServletContext();
        Object appName = context.getAttribute("appName");
        resp.getWriter().write(String.valueOf(appName));
    }
}

这里两个 Servlet 访问到的是同一个 ServletContext

获取 ServletContext 的常见方式

ServletContext context1 = getServletContext();
ServletContext context2 = getServletConfig().getServletContext();
ServletContext context3 = req.getServletContext();
ServletContext context4 = req.getSession().getServletContext();

入门阶段推荐直接记住这一种:

ServletContext context = getServletContext();

getRealPath() 

String rootPath = getServletContext().getRealPath("/");

它可以拿到 Web 应用部署后的真实路径,但要知道两个风险:

  • 部署方式变化后,真实路径可能变化
  • 某些打包或云部署环境下,真实路径不一定稳定

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇