跳转到内容

Blog

电商订单过期处理面试题复盘

这是我 2024 年遇到的一道电商面试题,题目大概是:

假如在某个时刻,也就是某一秒内,有数以千计的商品订单同时过期,数据库应该怎么处理?

这道题表面问的是“订单过期”,实际考察的是高并发场景下怎么避免数据库被瞬时写流量打穿。

一个比较稳妥的回答是:不要让数据库自己去扛所有过期判断,也不要用定时任务在同一秒扫大量订单。订单创建时写入延时队列,到期后由消费者接收消息,再做批量更新和后续事件分发。

订单过期一般不是一个难逻辑。比如订单超过 30 分钟未支付,就把状态从 WAIT_PAY 改成 CLOSED

真正的问题在于:如果某个秒级时间点有大量订单同时过期,系统会出现几个压力点:

  • 数据库短时间内出现大量 update
  • 如果使用定时任务扫描,查询范围可能很大。
  • 单条订单逐个更新会造成大量数据库往返。
  • 过期后还要释放库存、通知用户、同步缓存,容易把主流程拖慢。
  • 消息重复、消费失败、订单已支付等情况都需要正确处理。

所以这道题不能只回答“定时任务扫一下订单表”。那样在低并发下能跑,但在电商场景里不够稳。

我当时记录的主思路是四步:

订单创建 -> 写入延时队列 -> 到期消费 -> 批量关闭订单 -> 发布后续业务事件

可以画成这样:

创建订单
|
| 写订单表
v
发送延时消息:orderId + expireTime
|
| 到期后投递
v
订单过期消费者
|
| 聚合一小批订单
v
批量更新订单状态
|
| 更新成功后发布事件
v
释放库存 / 退款 / 通知用户 / 同步 ES 或缓存

核心目标是:把同一秒的大量过期订单,拆成可控的批量写入和异步事件处理。

订单创建成功后,系统先写入订单表,然后把订单 ID 和过期时间写入延时队列。

消息里通常包含:

order-expire-message.json
{
"orderId": "202401010001",
"orderType": "NORMAL",
"expireTime": "2024-01-01T10:30:00+08:00"
}

延时时间可以这样计算:

TTL = expireTime - now()

也就是说,如果订单 30 分钟后过期,就发送一条 30 分钟后投递的延时消息。

这里可以选不同的实现:

方案说明
RabbitMQ TTL + 死信队列到期后消息进入死信队列,由消费者处理
RocketMQ 延时消息原生支持延时消息,适合常见延时等级
Redis ZSet用过期时间作为 score,消费者按时间拉取
时间轮适合大量延时任务调度

面试时不用把某个 MQ 的细节讲太深,重点是说明:订单创建时就把“未来要关闭订单”这件事交给延时机制,而不是等数据库定时扫描。

当延时时间到达,消息会被投递到订单过期消费者。

消费者拿到消息后,不能直接无脑关闭订单,而是要先做状态校验:

收到 orderId
|
查询订单当前状态
|
如果已支付:忽略
如果已关闭:忽略
如果待支付且已超时:关闭

原因很简单:延时消息到达时,订单可能已经被用户支付了。

所以关闭订单必须是有条件的。不能只根据“消息到了”就关闭。

如果每条消息都单独更新一次数据库,几千条过期订单就会变成几千次 update

更好的做法是:消费者先把消息暂存到一个批量处理队列里,然后按数量或时间窗口触发批量更新。

常见触发条件:

  • 累积到 100-500 条订单。
  • 或者等待 100ms 左右。
  • 两个条件满足任意一个就执行批量更新。

可以理解为一个小型缓冲区:

消息 1 -> buffer
消息 2 -> buffer
消息 3 -> buffer
...
达到 300 条,批量 update

这样做的好处是明显的:

  • 减少数据库连接和网络往返。
  • 降低数据库瞬时写压力。
  • 消费者可以通过批次大小控制吞吐。
  • 后续可以水平扩展多个消费者。

批量更新订单状态时,一定要带上状态条件。

比如只关闭仍然处于待支付状态的订单:

batch-close-orders.sql
UPDATE orders
SET status = 'CLOSED',
close_reason = 'TIMEOUT',
closed_at = NOW()
WHERE id IN (...)
AND status = 'WAIT_PAY'
AND expire_time <= NOW();

这里的 status = 'WAIT_PAY' 很关键。

它能保证:

  • 已支付订单不会被误关闭。
  • 已关闭订单重复消费也不会重复更新。
  • 延时消息重复投递时,更新操作仍然安全。

这就是幂等。

面试里可以直接说:消息队列天然可能重复投递,所以订单关闭逻辑必须是幂等的。

订单状态批量更新完成后,不要在同一个流程里把所有后续业务都做完。

应该发布一个订单关闭事件:

order-closed-event.json
{
"eventType": "ORDER_CLOSED",
"reason": "TIMEOUT",
"orderIds": ["202401010001", "202401010002"]
}

然后由不同消费者处理后续业务:

  • 释放库存。
  • 发起退款。
  • 通知用户。
  • 同步订单状态到 ES。
  • 删除或刷新缓存。
  • 写入业务日志。

这样可以把“关闭订单”和“关闭订单后要做什么”拆开。

关闭订单是核心链路,必须尽快完成;释放库存、通知用户、同步缓存这些动作可以异步扩展。

定时任务不是不能用,但它不适合直接承担高并发订单过期主链路。

如果用定时任务每秒扫一次订单表,大概会遇到几个问题:

  • 查询条件依赖 expire_time,数据量大时压力明显。
  • 某一秒过期订单过多时,任务执行时间不可控。
  • 任务失败后需要补偿。
  • 多实例部署时要处理分布式锁和重复扫描。
  • 单表订单量大时,还要考虑分库分表后的扫描范围。

定时任务更适合做兜底补偿,比如每隔一段时间扫描仍然处于 WAIT_PAY 且已经超时的订单,补偿可能丢失的延时消息。

所以更完整的方案是:

延时队列处理主流程
定时任务做兜底补偿

题目里说某一秒有数以千计订单过期,这其实就是一个瞬时流量峰值。

除了批量更新,还可以做一些削峰处理:

  • 多消费者并发消费,但限制每个消费者批量写入频率。
  • 将订单按业务线、商户、分库分表键分片处理。
  • 批量更新失败时拆分批次重试。
  • 对数据库连接池和 MQ 消费速度设置上限。
  • 监控积压量,必要时临时扩容消费者。

关键思想是:MQ 可以积压,消费者可以慢慢处理,但数据库不能被突然打爆。

这道题可以按下面这段话回答:

我不会让数据库在某一秒直接承受大量订单过期更新。订单创建成功后,会把订单 ID 和过期时间写入延时队列,TTL 设置为 expireTime - now()。消息到期后投递给订单过期消费者,消费者先校验订单是否仍是待支付状态,再把消息放入批量缓冲区,按固定数量或固定时间窗口统一批量更新订单状态。更新时 SQL 会带上 status = WAIT_PAY 和过期时间条件,保证幂等,避免误关已支付订单。订单关闭成功后再发布订单关闭事件,由库存、退款、通知、缓存同步等消费者异步处理。定时任务只作为兜底补偿,不作为主流程。

这段回答基本覆盖了面试官想听的几个关键词:

  • 延时队列。
  • 批量处理。
  • 幂等。
  • 削峰。
  • 事件驱动。
  • 兜底补偿。

这道题的重点不是“怎么把订单改成过期”,而是怎么在大量订单同时过期时保护数据库。

比较合理的设计是:

订单创建时写延时队列
到期后消费者接收消息
消费者聚合订单并批量更新
更新时保证幂等
关闭后发布业务事件
定时任务做兜底补偿

电商系统里很多问题都不是单点逻辑难,而是峰值、幂等、补偿、解耦这些工程问题难。这个题目正好把这些点串到了一起。

Vue 请求后端数据与跨域问题

这篇文章迁移自我早年写在博客园的一篇记录。当时遇到的问题很直接:后端接口已经写好了,Vue 前端应该怎么请求数据?请求时报跨域又应该怎么处理?

现在回头看,这其实是前后端分离开发里非常典型的一步:前端项目跑在一个端口,后端服务跑在另一个端口,浏览器因为同源策略限制,默认不允许它们随便互相请求。

这篇文章按现在的写法重新整理一遍:先用 axios 发起请求,再抽出请求模块,最后处理跨域问题。

假设后端提供了一个用户列表接口:

GET http://localhost:8088/user

前端 Vue 项目运行在另一个地址,例如:

http://localhost:8080/

这两个地址的协议、域名或端口只要有一个不同,就不是同源。这里端口不同,所以浏览器会把它们视为跨域请求。

前端请求接口可以使用 axios

Terminal window
npm install axios

安装后,在需要请求数据的 Vue 页面中引入:

import axios from 'axios';

如果项目使用的是 Vue 2 或 Options API,可以先在 methods 中写一个简单请求方法。

export default {
methods: {
fetchUsers() {
axios
// 这里填写后端接口地址
.get('http://localhost:8088/user')
.then(function (response) {
// 请求成功后,后端返回的数据在 response.data 中
console.log(response.data);
})
.catch(function (error) {
// 请求失败时会进入 catch
console.log(error);
})
.finally(function () {
// 不管成功还是失败,finally 都会执行
console.log('请求结束');
});
},
},
};

页面上可以绑定一个按钮测试请求:

<button @click="fetchUsers">请求用户数据</button>

点击按钮后,打开浏览器控制台。如果后端接口正常,并且跨域已经允许,就能在控制台看到返回数据。

如果你更喜欢同步风格,也可以用 async / await

import axios from 'axios';
export default {
methods: {
async fetchUsers() {
try {
// await 会等待请求完成
const response = await axios.get('http://localhost:8088/user');
// 后端真正返回的数据通常放在 response.data
console.log(response.data);
} catch (error) {
// 网络错误、接口错误、跨域错误都会进入这里
console.log(error);
}
},
},
};

这种写法在业务代码里更容易读,尤其是请求成功后还有多步处理时。

刚开始写一个请求时,直接在页面里写完整地址没有问题。

但接口一多,就会出现几个问题:

  • 每个页面都要写 http://localhost:8088
  • 请求地址散落在各个组件里。
  • 后续换服务器地址时,需要到处改。
  • 请求头、错误处理、登录 token 等逻辑难以统一。

所以更常见的做法是:把 axios 实例单独封装起来,再把具体接口拆到 api 目录里。

可以按下面的方式组织前端请求代码:

  • 文件夹src/
    • 文件夹api/
      • user.js
    • 文件夹utils/
      • request.js
    • 文件夹views/
      • UserList.vue

这里的职责可以这样理解:

  • utils/request.js:创建 axios 实例,统一配置基础地址、超时时间、拦截器。
  • api/user.js:按业务模块封装用户相关接口。
  • views/UserList.vue:页面组件,只关心调用哪个接口,不关心底层请求细节。

src/utils/request.js 中写入:

import axios from 'axios';
const request = axios.create({
// 接口服务器地址
// 后续接口只需要写 /user、/login 这类路径
baseURL: 'http://localhost:8088',
// 超时时间,单位是毫秒
timeout: 10000,
});
// 请求拦截器:请求发出前会先经过这里
request.interceptors.request.use(
function (config) {
// 如果后续有 token,可以在这里统一添加
// config.headers.Authorization = `Bearer ${token}`;
return config;
},
function (error) {
return Promise.reject(error);
}
);
// 响应拦截器:后端响应后会先经过这里
request.interceptors.response.use(
function (response) {
// 这里先直接返回 response
// 如果后端有统一结构,也可以只返回 response.data
return response;
},
function (error) {
// 可以在这里统一处理错误提示、登录过期等问题
return Promise.reject(error);
}
);
export default request;

这样封装后,页面里就不需要每次都写完整后端地址了。

src/api/user.js 中写:

import request from '../utils/request';
export function getUserList(params = {}) {
return request({
// axios 里字段名是 method,不是 methods
method: 'GET',
// 会和 baseURL 拼接成 http://localhost:8088/user
url: '/user',
// GET 请求参数
params,
});
}

这里要注意一个细节:axios 配置里的请求方式字段是 method,不是 methods

在页面组件中引入接口方法:

import { getUserList } from '../api/user';
export default {
data() {
return {
tableData: [],
};
},
methods: {
async loadUsers() {
try {
const response = await getUserList();
// 如果响应拦截器返回的是完整 response,就从 response.data 取数据
this.tableData = response.data;
} catch (error) {
console.log(error);
}
},
},
};

如果希望页面创建时自动请求,可以在 created 生命周期中调用:

import { getUserList } from '../api/user';
export default {
data() {
return {
tableData: [],
};
},
created() {
// 页面创建时加载数据
this.loadUsers();
},
methods: {
async loadUsers() {
const response = await getUserList();
this.tableData = response.data;
},
},
};

如果要在模板中展示:

<template>
<ul>
<li v-for="user in tableData" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>

实际字段名要根据后端返回数据决定。

前端请求后端时,如果浏览器控制台出现类似错误:

Access to XMLHttpRequest at 'http://localhost:8088/user'
from origin 'http://localhost:8080'
has been blocked by CORS policy

这就是跨域问题。

它不是 axios 的问题,也不是 Vue 的问题,而是浏览器的同源策略在生效。

同源要求三者完全一致:

  • 协议相同,例如都是 http
  • 域名相同,例如都是 localhost
  • 端口相同,例如都是 8080

下面两个地址端口不同,所以不是同源:

前端:http://localhost:8080
后端:http://localhost:8088

浏览器会拦截前端 JavaScript 对后端的请求,除非后端明确允许这个来源访问。

如果后端是 Spring Boot,可以在 Controller 上添加 @CrossOrigin

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@CrossOrigin
public class UserController {
@GetMapping("/user")
public List<User> listUsers() {
return userService.listUsers();
}
}

这样写表示这个 Controller 允许跨域访问。

如果想限制来源,可以写得更明确:

@CrossOrigin(origins = "http://localhost:8080")
@RestController
public class UserController {
// ...
}

这比完全放开更安全。

如果接口很多,不想每个 Controller 都写 @CrossOrigin,也可以做全局配置。

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
// 哪些接口路径允许跨域
.addMapping("/**")
// 允许哪个前端地址访问
.allowedOrigins("http://localhost:8080")
// 允许哪些请求方法
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// 允许携带哪些请求头
.allowedHeaders("*");
}
}

开发阶段可以先这样配置。正式环境中,allowedOrigins 不建议随便写成 *,最好明确填写真实前端域名。

开发环境也可以通过前端代理解决跨域。

如果使用 Vue CLI,可以在 vue.config.js 中配置:

module.exports = {
devServer: {
proxy: {
'/api': {
// 后端服务地址
target: 'http://localhost:8088',
// 是否改变请求来源
changeOrigin: true,
// 把 /api/user 重写成 /user
pathRewrite: {
'^/api': '',
},
},
},
},
};

这时 axios 的基础地址可以改成:

const request = axios.create({
baseURL: '/api',
});

请求:

request({
method: 'GET',
url: '/user',
});

浏览器实际请求的是前端开发服务器:

http://localhost:8080/api/user

再由前端开发服务器转发给后端:

http://localhost:8088/user

这样浏览器看到的是同源请求,就不会触发跨域限制。

浏览器跨域是浏览器限制。你用 Postman、Apifox、curl 能请求成功,不代表浏览器里也能请求成功。

axios 报错是不是后端没返回数据

Section titled “axios 报错是不是后端没返回数据”

不一定。跨域错误时,请求可能已经被浏览器拦截,前端拿不到正常响应。要先看浏览器控制台的 Network 和 Console。

如果使用普通函数,this 可能发生变化。

getUserList().then(function (response) {
// 这里的 this 不一定是 Vue 组件实例
this.tableData = response.data;
});

可以改成箭头函数:

getUserList().then((response) => {
// 箭头函数不会重新绑定 this
this.tableData = response.data;
});

或者直接使用 async / await

Vue 请求后端接口时,可以按这个顺序处理:

  1. 使用 axios 先完成一次最简单的请求。
  2. 确认后端接口能正常返回数据。
  3. 如果浏览器报跨域,让后端配置 CORS,或者在开发环境使用代理。
  4. 当接口变多后,把 axios 封装成统一的 request 模块。
  5. 按业务拆分 API 文件,页面只调用方法,不直接拼接口地址。

这篇旧文当年只是为了解决“前端怎么拿到后端数据”这个问题。现在重新整理后,它更像是一条前后端分离开发的入门路径:先跑通,再封装,最后处理跨域和工程结构。

原文记录:vue请求后端数据和跨域问题

ServletConfig 接口介绍

这篇文章迁移自我早年写在博客园的一篇记录。当时主要是在学习 Servlet 初始化参数:Servlet 容器初始化 Servlet 时,会创建一个 ServletConfig 对象,并把它交给当前 Servlet 使用。

简单说,ServletConfig 适合保存“只属于某一个 Servlet 的配置”。比如某个 Servlet 自己需要的用户名、开关、路径、默认参数等。

当 Web 容器创建并初始化 Servlet 时,会为这个 Servlet 准备一个 ServletConfig 对象。这个对象里包含当前 Servlet 的初始化信息,也可以通过它拿到整个 Web 应用的 ServletContext

需要记住两点:

  • 一个 Web 应用里可以有多个 Servlet,也就可以有多个 ServletConfig 对象。
  • 一个 Servlet 只对应一个 ServletConfig 对象,所以 Servlet 初始化参数默认只对当前 Servlet 有效。

如果把 Web 应用理解成一个项目,那么:

  • ServletContext 更像“整个项目的全局上下文”。
  • ServletConfig 更像“某一个 Servlet 自己的配置说明”。

ServletConfig 常用方法不多,重点是下面这几个:

方法作用
String getInitParameter(String name)根据参数名获取当前 Servlet 的初始化参数值
Enumeration<String> getInitParameterNames()获取当前 Servlet 所有初始化参数名
ServletContext getServletContext()获取当前 Web 应用的 ServletContext 对象
String getServletName()获取当前 Servlet 名称,也就是 web.xml<servlet-name> 的值

这里最常用的是 getInitParameter()getInitParameterNames()。前者适合读取单个配置,后者适合遍历所有配置。

Servlet 里有两类参数很容易混在一起:

配置位置所属对象读取方式生效范围
<context-param>ServletContextgetServletContext().getInitParameter()整个 Web 应用
<servlet> 里的 <init-param>ServletConfiggetServletConfig().getInitParameter()当前 Servlet

也就是说,context-param 不是当前 Servlet 的私有配置,它属于整个 Web 应用。ServletConfig#getServletContext() 只是让你可以从当前 Servlet 拿到全局上下文,并不代表这些全局参数属于 ServletConfig

这个区别非常重要。很多初学 Servlet 的时候,会把“通过 ServletConfig 拿到 ServletContext,再读取全局参数”误认为是在读取 Servlet 自己的初始化参数。

先看全局参数的写法。下面的 admin-emailadmin-nameadmin-password 都配置在 <context-param> 中,因此它们属于整个 Web 应用。

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>Servlet</servlet-name>
<servlet-class>main.java.com.Servlet</servlet-class>
</servlet>
<context-param>
<param-name>admin-email</param-name>
<param-value>123456@qq.com</param-value>
</context-param>
<context-param>
<param-name>admin-name</param-name>
<param-value>xiaoxi</param-value>
</context-param>
<context-param>
<param-name>admin-password</param-name>
<param-value>123456</param-value>
</context-param>
<servlet-mapping>
<servlet-name>Servlet</servlet-name>
<url-pattern>/Servlet</url-pattern>
</servlet-mapping>
</web-app>

读取这些全局参数时,需要先拿到 ServletContext

Servlet.java
package main.java.com;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
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;
@WebServlet(name = "Servlet", value = "/Servlet")
public class Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain;charset=UTF-8");
ServletConfig config = getServletConfig();
ServletContext servletContext = config.getServletContext();
String adminEmail = servletContext.getInitParameter("admin-email");
String adminName = servletContext.getInitParameter("admin-name");
String password = servletContext.getInitParameter("admin-password");
response.getWriter().println("admin-email: " + adminEmail);
response.getWriter().println("admin-name: " + adminName);
response.getWriter().println("admin-password: " + password);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

这段代码里虽然先调用了 getServletConfig(),但真正读取参数的是 servletContext.getInitParameter()。所以它读取的是全局初始化参数。

使用 web.xml 配置当前 Servlet 参数

Section titled “使用 web.xml 配置当前 Servlet 参数”

如果希望参数只属于当前 Servlet,就应该把参数写到 <servlet> 里的 <init-param> 中。

web.xml
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>java.com.MyServlet</servlet-class>
<init-param>
<param-name>name</param-name>
<param-value>xiaoxi</param-value>
</init-param>
<init-param>
<param-name>admin</param-name>
<param-value>xiaoxi</param-value>
</init-param>
</servlet>

这种参数才是 ServletConfig 最典型的使用场景。

MyServlet.java
ServletConfig config = getServletConfig();
String name = config.getInitParameter("name");
String admin = config.getInitParameter("admin");

如果另一个 Servlet 也想使用同名参数,需要在另一个 Servlet 的配置里重新声明。Servlet 私有参数不会自动共享。

除了 web.xml,也可以直接使用 @WebServlet@WebInitParam 配置当前 Servlet 的初始化参数。

这种方式更适合简单项目或示例代码,因为配置和 Servlet 类写在一起,阅读起来更直观。

HelloServlet.java
package main.java.com;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
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;
import java.util.Enumeration;
@WebServlet(
name = "helloServlet",
value = "/helloServlet",
initParams = {
@WebInitParam(name = "name", value = "测试"),
@WebInitParam(name = "admin", value = "123456")
}
)
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
ServletConfig config = getServletConfig();
String servletName = config.getServletName();
Enumeration<String> initParameterNames = config.getInitParameterNames();
PrintWriter writer = response.getWriter();
writer.write("servletName: " + servletName + "<br/>");
while (initParameterNames.hasMoreElements()) {
String initParamName = initParameterNames.nextElement();
String initParamValue = config.getInitParameter(initParamName);
writer.write(initParamName + ": " + initParamValue + "<br/>");
}
writer.close();
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

这里的 initParams 配置只对 HelloServlet 生效。别的 Servlet 不能直接通过自己的 ServletConfig 读取这些参数。

如果项目很小,或者只是练习 Servlet,使用注解会比较方便。

如果项目配置比较多,或者需要把代码和配置分离,使用 web.xml 会更清晰。尤其是早期 Java Web 项目里,很多 Servlet、Filter、Listener 都会统一写在 web.xml 中。

可以简单按下面的方式判断:

场景推荐方式
示例代码、学习项目@WebServlet
配置较少的小项目@WebServletweb.xml 都可以
多个 Servlet 需要统一管理web.xml
希望参数不写死在 Java 类里web.xml

两种方式本质上都是告诉 Servlet 容器:这个 Servlet 叫什么、映射到哪个路径、初始化时带哪些参数。

第一,context-paraminit-param 不要混用。

如果一个参数是全站通用的,比如站点名称、管理员邮箱、上传目录,可以放在 <context-param> 中。如果一个参数只服务于某个 Servlet,就放在对应 Servlet 的 <init-param> 中。

第二,读取参数时要找对对象。

// 读取当前 Servlet 的初始化参数
getServletConfig().getInitParameter("name");
// 读取整个 Web 应用的全局参数
getServletContext().getInitParameter("admin-email");

第三,输出中文时记得设置响应编码。

response.setContentType("text/html;charset=UTF-8");

如果不设置编码,浏览器里可能会出现中文乱码。

第四,注意包名差异。

早期 Servlet 项目常见包名是 javax.servlet。如果使用的是 Tomcat 10、Spring Boot 3 或 Jakarta EE 新版本,包名会变成 jakarta.servlet

// 旧版本常见写法
import javax.servlet.ServletConfig;
// 新版本常见写法
import jakarta.servlet.ServletConfig;

学习旧项目或迁移项目时,这个差异很常见。

ServletConfig 的核心作用,是保存并读取当前 Servlet 的初始化参数。

需要特别分清楚:

  • ServletConfig 面向当前 Servlet。
  • ServletContext 面向整个 Web 应用。
  • <init-param> 是 Servlet 私有配置。
  • <context-param> 是 Web 应用全局配置。
  • 注解里的 initParams 只对当前 Servlet 生效。

把这几个概念理顺之后,Servlet 初始化参数就不难了。真正容易出错的地方,往往不是 API 本身,而是没有分清“这个配置到底属于谁”。

原文记录:ServletConfig接口介绍