跳转到内容

Java

标签「Java」下的 2 篇文章

Java 校招面试题复盘清单

这是一份 Java 校招面试题复盘清单。

它不适合当作“背诵稿”逐字记忆,更适合当作复习地图:先知道面试会问哪些方向,再把每个问题整理成可以讲清楚的核心答案。

Java 校招面试通常会围绕三部分展开:

模块占比重点
技术基础约 40%Java、Spring、MySQL、Redis、JVM、并发
项目深挖约 40%项目背景、技术选型、难点、优化、问题排查
学习能力约 20%最近在学什么、为什么做 Java、如何解决问题

如果项目里有 Elasticsearch,那么 ES 往往会成为面试官重点追问的方向。

HashMap 底层主要是数组、链表和红黑树。

当放入一个 key-value 时,会先根据 key 的 hashCode() 计算 hash,再定位到数组下标。如果该位置没有元素,就直接放入;如果已经有元素,就会形成链表或红黑树。

在 Java 8 之后,当链表长度达到一定阈值,并且数组容量足够大时,链表会转换为红黑树,用来提高查询效率。

可以这样回答:

HashMap 通过 hash 定位数组下标,数组中每个位置叫 bucket。发生 hash 冲突时,Java 8 以前主要用链表,Java 8 之后链表过长会转为红黑树。扩容时会重新计算元素位置,所以 HashMap 的性能和初始容量、负载因子、hash 分布都有关系。

HashMap 没有做同步控制,多线程同时读写时可能出现数据覆盖、状态不一致、扩容异常等问题。

常见风险:

  • 多个线程同时 put,可能覆盖彼此写入。
  • 扩容时结构变化,其他线程同时访问可能拿到异常结果。
  • 统计数量 size 可能不准确。

所以多线程场景通常使用 ConcurrentHashMap

ConcurrentHashMap 如何实现线程安全

Section titled “ConcurrentHashMap 如何实现线程安全”

Java 8 中,ConcurrentHashMap 主要通过 CAS 和 synchronized 保证线程安全。

它不是给整张表加一把大锁,而是尽量缩小锁粒度。

常见回答:

Java 8 的 ConcurrentHashMap 底层也是数组、链表和红黑树。插入时,如果桶为空,会通过 CAS 放入节点;如果桶不为空,会对桶头节点加 synchronized,只锁当前桶。这样既保证线程安全,又比 Hashtable 整表加锁性能更好。

ArrayList 底层是动态数组,LinkedList 底层是双向链表。

对比项ArrayListLinkedList
底层结构动态数组双向链表
随机访问快,按下标访问 O(1)慢,需要遍历
中间插入删除需要移动元素找到节点后修改指针
内存占用相对较少每个节点要存前后指针
常用场景查询多插入删除多,但实际也要看位置

面试里要注意:不要简单说 LinkedList 插入删除一定快。因为如果要先按索引找到位置,遍历本身也有成本。

String、StringBuilder、StringBuffer 的区别

Section titled “String、StringBuilder、StringBuffer 的区别”

String 是不可变对象,每次修改都会产生新字符串。

StringBuilder 是可变字符序列,线程不安全,但性能较好。

StringBuffer 也是可变字符序列,方法加了同步,线程安全,但性能通常低于 StringBuilder

常见选择:

  • 少量字符串拼接:直接用 String
  • 单线程大量拼接:用 StringBuilder
  • 多线程共享拼接对象:用 StringBuffer,但实际业务中较少这样用。

== 比较的是两边是否相等。

对于基本类型,比较的是值。

对于引用类型,比较的是对象地址。

equals() 是对象方法,默认实现也是比较地址,但很多类会重写它,比如 String 会比较字符串内容。

equals-demo.java
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true

Java 异常体系的顶层是 Throwable

它下面主要分为:

  • Error:严重错误,程序通常不主动处理,比如 OutOfMemoryError
  • Exception:程序可以捕获和处理的异常。

Exception 又分为:

  • checked exception:编译期异常,必须处理或声明抛出。
  • unchecked exception:运行时异常,继承自 RuntimeException,例如空指针、数组越界。

面试里可以补一句:业务开发中不要滥用异常控制正常流程,异常更适合表示非预期情况。

反射是 Java 在运行时获取类信息、创建对象、调用方法、访问字段的能力。

常见应用场景:

  • Spring 创建 Bean、依赖注入。
  • MyBatis 映射对象字段。
  • 注解解析。
  • 测试框架调用测试方法。
  • 动态代理。

反射灵活,但也有缺点:性能相对普通调用更低,可读性和安全性更差。

进程是操作系统资源分配的基本单位。一个应用程序运行起来通常就是一个进程。

线程是 CPU 调度的基本单位,一个进程中可以包含多个线程。

可以这样回答:

进程拥有独立的内存空间,线程共享同一进程的内存资源。线程切换成本通常低于进程,但共享数据也会带来线程安全问题。

synchronized 可以修饰方法或代码块,用来保证同一时间只有一个线程进入临界区。

它依赖对象监视器锁,也就是 monitor。

进入同步代码块时,线程尝试获取对象锁;执行完或异常退出时释放锁。

可以补充:

  • 修饰普通方法,锁的是当前对象 this
  • 修饰静态方法,锁的是当前类的 Class 对象。
  • 修饰代码块,可以指定锁对象。

volatile 主要有两个作用:

  • 保证变量对多线程的可见性。
  • 禁止指令重排序。

它不能保证复合操作的原子性。

比如 count++ 包含读取、加一、写回,不是一个原子操作,所以只加 volatile 仍然不安全。

常见场景:

  • 状态标志位。
  • 单例模式双重检查锁中的实例变量。

线程池是提前创建并管理一组线程,任务来了以后交给线程池执行。

使用线程池的原因:

  • 避免频繁创建和销毁线程。
  • 控制并发线程数量。
  • 提高响应速度。
  • 统一管理任务队列、拒绝策略和线程生命周期。

ThreadPoolExecutor 常见核心参数:

参数作用
corePoolSize核心线程数
maximumPoolSize最大线程数
keepAliveTime非核心线程空闲存活时间
unit时间单位
workQueue任务队列
threadFactory线程创建工厂
handler拒绝策略

执行流程可以概括为:

核心线程未满 -> 创建核心线程
核心线程已满 -> 放入任务队列
队列满了 -> 创建非核心线程
线程数达到最大且队列也满 -> 执行拒绝策略

死锁是多个线程互相持有对方需要的资源,导致都无法继续执行。

死锁常见四个条件:

  • 互斥。
  • 请求并保持。
  • 不可剥夺。
  • 循环等待。

避免方式:

  • 固定加锁顺序。
  • 减少锁范围。
  • 使用超时锁。
  • 避免嵌套锁。
  • 使用并发工具类代替手写锁。

JVM 运行时数据区主要包括:

  • 程序计数器。
  • Java 虚拟机栈。
  • 本地方法栈。
  • 堆。
  • 方法区。

线程私有:

  • 程序计数器。
  • Java 虚拟机栈。
  • 本地方法栈。

线程共享:

  • 堆。
  • 方法区。

栈主要存放方法调用相关信息,比如局部变量表、操作数栈、方法出口等。

堆主要存放对象实例,是垃圾回收重点关注的区域。

对比项
线程关系线程私有线程共享
存储内容方法调用、局部变量对象实例
生命周期随方法调用入栈出栈由 GC 管理
常见错误StackOverflowErrorOutOfMemoryError

垃圾回收是 JVM 自动回收不再使用对象的机制。

判断对象是否可回收,主流方法是可达性分析:从 GC Roots 出发,能到达的对象是存活对象,不能到达的对象可以被回收。

常见 GC Roots:

  • 虚拟机栈中的引用。
  • 方法区中的静态变量引用。
  • 常量引用。
  • 本地方法栈中的引用。

常见算法:

  • 标记-清除:先标记垃圾,再清除,可能产生内存碎片。
  • 复制算法:把存活对象复制到另一块区域,适合新生代。
  • 标记-整理:标记后整理存活对象,减少碎片。
  • 分代收集:按对象生命周期分区,不同区域用不同算法。

OOM 是内存不足导致的错误。

常见原因:

  • 创建了大量对象,堆空间不足。
  • 大对象过多。
  • 内存泄漏,旧对象一直被引用。
  • 线程过多导致栈空间不足。
  • 元空间加载类过多。

排查时通常会看日志、堆 dump、GC 情况和对象引用链。

常见关注点:

  • 堆大小:-Xms-Xmx
  • 新生代大小:-Xmn
  • 元空间大小:-XX:MetaspaceSize
  • GC 收集器选择。
  • GC 日志。
  • 停顿时间和吞吐量。

调优不是盲目改参数,而是先看现象:内存是否够、GC 是否频繁、停顿是否过长、对象是否异常增长。

IoC 是控制反转。

原本对象由程序自己创建和管理,现在交给 Spring 容器创建和管理。

DI 依赖注入是 IoC 的一种实现方式。比如一个 Service 依赖 Mapper,不需要自己 new,而是由 Spring 注入。

AOP 是面向切面编程,用来把通用逻辑从业务代码中抽离出来。

常见场景:

  • 日志。
  • 权限校验。
  • 事务。
  • 监控统计。
  • 接口耗时。

核心思想是:不修改业务方法本身,在方法执行前后织入增强逻辑。

简化流程:

实例化 -> 属性注入 -> 初始化前后处理 -> 初始化方法 -> 使用 -> 销毁

常见扩展点:

  • 构造方法。
  • 属性填充。
  • BeanPostProcessor
  • InitializingBeaninit-method
  • DisposableBeandestroy-method

Spring Boot 主要解决传统 Spring 项目配置繁琐的问题。

优势:

  • 自动配置。
  • 内置 Web 容器。
  • starter 依赖简化。
  • 快速创建项目。
  • 方便监控和部署。

一句话回答:

Spring Boot 让 Spring 项目更容易启动和维护,它通过自动配置和 starter 机制减少大量 XML 或手动配置。

自动配置的核心是根据类路径、配置文件和条件注解,自动创建合适的 Bean。

常见关键词:

  • starter。
  • auto configuration。
  • @EnableAutoConfiguration
  • 条件注解,比如 @ConditionalOnClass@ConditionalOnMissingBean

面试中可以这样说:

Spring Boot 会根据引入的依赖和当前环境判断是否满足条件,如果满足,就把对应配置类里的 Bean 注册到容器中。

RESTful API 是一种接口设计风格。

核心思想:

  • 使用 URL 表示资源。
  • 使用 HTTP 方法表示操作。
  • 使用状态码表示结果。

示例:

GET /users/1 查询用户
POST /users 创建用户
PUT /users/1 更新用户
DELETE /users/1 删除用户

索引是帮助 MySQL 快速查找数据的数据结构。

可以理解为书的目录。没有索引时,数据库可能要全表扫描;有索引时,可以更快定位数据。

索引能提高查询效率,但会增加写入成本和存储空间。

B+Tree 适合磁盘存储和范围查询。

原因:

  • 树高度低,减少磁盘 IO。
  • 非叶子节点只存索引,能放更多 key。
  • 叶子节点之间有链表,范围查询效率高。
  • 查询性能稳定。

覆盖索引指查询需要的字段都能从索引中获得,不需要回表查询。

比如有联合索引 (name, age)

SELECT name, age FROM user WHERE name = 'xiaoxi';

如果查询字段都在索引里,就可能走覆盖索引。

常见情况:

  • 对索引列使用函数。
  • 对索引列做计算。
  • 使用左模糊匹配,例如 LIKE '%abc'
  • 联合索引不符合最左前缀原则。
  • 隐式类型转换。
  • OR 条件使用不当。

事务是一组数据库操作的集合,要么全部成功,要么全部失败。

比如下单时:

  • 创建订单。
  • 扣减库存。
  • 扣减余额。

这些操作应该作为一个整体处理。

特性含义
Atomicity 原子性事务要么全部成功,要么全部失败
Consistency 一致性事务前后数据满足约束
Isolation 隔离性并发事务之间互相隔离
Durability 持久性事务提交后数据持久保存

MVCC 是多版本并发控制。

它通过保存数据的多个版本,让读写尽量不互相阻塞。

在 InnoDB 中,MVCC 主要依赖:

  • 隐藏字段。
  • undo log。
  • ReadView。

常见作用是支持可重复读和快照读。

常见原因:

  • 数据主要在内存中。
  • 使用高效数据结构。
  • 单线程命令执行避免大量锁竞争。
  • IO 多路复用。
  • C 语言实现,执行效率高。

常见类型:

  • String。
  • Hash。
  • List。
  • Set。
  • Sorted Set。
  • Bitmap。
  • HyperLogLog。
  • Geo。
  • Stream。

缓存穿透是请求的数据在缓存和数据库中都不存在,导致请求持续打到数据库。

解决方式:

  • 缓存空对象。
  • 布隆过滤器。

缓存击穿是热点 Key 过期瞬间,大量并发请求同时打到数据库。

解决方式:

  • 互斥锁。
  • 逻辑过期。

缓存雪崩是大量 Key 同时过期,或者 Redis 整体不可用,导致大量请求涌向数据库。

解决方式:

  • 过期时间加随机值。
  • 多级缓存。
  • 限流降级。
  • Redis 高可用。

Elasticsearch 是一个分布式搜索和分析引擎,常用于全文搜索、日志检索、商品搜索等场景。

它底层基于 Lucene,对外提供 REST API,支持分布式、倒排索引和复杂查询。

倒排索引是从词到文档的映射。

普通索引更像:

文档 -> 包含哪些词

倒排索引更像:

词 -> 出现在哪些文档中

这就是 ES 做全文搜索快的重要原因。

index 类似一类数据的集合。

document 是一条 JSON 数据。

type 在早期版本中用于区分类型,但新版本已经逐步移除,不建议在新项目中依赖 type。

可以类比:

ES关系型数据库
indextable 或 database 的某种集合概念
documentrow
fieldcolumn

DSL 是 Elasticsearch 的查询语言,使用 JSON 描述查询条件。

示例:

es-dsl.json
{
"query": {
"match": {
"title": "redis"
}
}
}

常见条件:

  • must:必须匹配,影响评分。
  • should:可选匹配,可能影响评分。
  • filter:必须匹配,但不参与评分,适合过滤条件。
  • must_not:必须不匹配。

为什么 Elasticsearch 搜索比 MySQL 快

Section titled “为什么 Elasticsearch 搜索比 MySQL 快”

ES 在全文搜索场景下更快,主要因为它使用倒排索引。

MySQL 更擅长结构化数据查询和事务处理,全文检索不是它最核心的场景。

可以这样回答:

ES 会先对文本分词,再建立词到文档的倒排索引。查询关键词时,可以快速定位包含该词的文档。MySQL B+Tree 索引更适合精确匹配和范围查询,对复杂全文搜索、相关性评分和分词检索不如 ES 合适。

Docker 是容器化技术,可以把应用和依赖打包成镜像,再以容器方式运行。

它解决了“本地能跑,服务器不能跑”的环境一致性问题。

对比项Docker虚拟机
隔离方式进程级隔离硬件级虚拟化
启动速度
资源占用较少较多
系统内核共享宿主机内核每个虚拟机有完整操作系统
适用场景应用部署、微服务强隔离、多系统环境

常见命令:

linux-commands.sh
ls
cd
pwd
mkdir
rm
cp
mv
cat
tail -f app.log
grep "error" app.log
ps -ef
top
df -h
free -m
chmod
chown

面试时如果结合项目部署经历回答,会比单纯背命令更好。

REST API 是基于 REST 风格设计的接口。

它通常使用 HTTP 协议,通过 URL 表示资源,通过 HTTP 方法表示操作。

这个问题和 Spring 里的 RESTful API 本质相同。

微服务是把一个大系统拆成多个小服务,每个服务负责一个相对独立的业务能力。

优点:

  • 服务独立部署。
  • 技术栈可以更灵活。
  • 方便水平扩展。
  • 团队边界更清晰。

缺点:

  • 服务间调用复杂。
  • 分布式事务困难。
  • 监控、链路追踪、部署复杂度提高。

API 网关是系统入口,负责把外部请求转发到内部服务。

常见功能:

  • 路由转发。
  • 鉴权。
  • 限流。
  • 熔断。
  • 日志。
  • 跨域处理。

如果你的项目重点是 Elasticsearch,面试官很可能会围绕项目继续追问。

可以提前准备这些问题:

问题准备方向
为什么项目要用 ESMySQL 搜索能力不足、全文检索、分词、排序、性能
数据怎么同步到 ES同步写、异步消息、定时补偿
ES 和 MySQL 数据不一致怎么办重试、补偿任务、最终一致性
索引怎么设计index、mapping、分词器、字段类型
搜索结果怎么排序相关性评分、业务权重、时间、热度
查询慢怎么优化filter、分页限制、字段选择、索引设计

面试时不要只说“我用了 ES”。更好的说法是:

我在项目中用 Elasticsearch 解决全文检索问题。MySQL 更适合事务和结构化查询,但对分词搜索、相关性排序支持有限。所以把需要搜索的数据同步到 ES,通过倒排索引提升搜索效率。项目里还需要考虑 MySQL 和 ES 的数据一致性,比如通过消息队列异步同步,并配合定时任务补偿。

学习能力问题通常不会太难,但很考验真实感。

常见问题:

  • 最近在学什么技术?
  • 为什么选择 Java?
  • 遇到不会的问题怎么解决?
  • 看过哪些技术文档?
  • 项目里最有收获的地方是什么?

回答建议:

  • 不要只说“我在学 Java”。
  • 要说具体学了什么、为什么学、怎么实践。
  • 最好能和项目或面试岗位关联起来。

例如:

最近我在补 Redis 和 Elasticsearch。Redis 主要关注缓存穿透、击穿、雪崩以及数据结构的使用场景;Elasticsearch 主要学习倒排索引、DSL 查询和数据同步。因为我的项目里有搜索场景,所以我想把搜索链路和缓存链路都理解得更完整。

如果时间有限,可以按这个顺序准备:

  1. Java 基础:集合、字符串、异常、反射。
  2. MySQL:索引、事务、MVCC、索引失效。
  3. Redis:数据结构、缓存问题、常用命令。
  4. Spring Boot:IoC、AOP、自动配置、REST API。
  5. JVM:内存结构、GC、OOM。
  6. 并发:线程池、锁、volatile、死锁。
  7. Elasticsearch 项目:倒排索引、DSL、数据同步、搜索优化。
  8. Docker 和 Linux:能讲清楚部署和常用命令即可。

Java 校招面试不是只背八股。

基础题要能答清楚概念,项目题要能讲清楚自己做了什么、为什么这样做、遇到了什么问题、怎么解决。

这份清单的重点不是一次性背完,而是把每个问题都整理成三层:

一句话结论
核心原理
项目或使用场景

这样回答时会更稳,也更像真的理解过。

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接口介绍