☰
2025最新Java面试题精讲:从Tomcat到Shiro的原理链路串讲
2026/10/15 14:03:51 网站建设 项目流程

简介:面向Java后端开发者与2025年求职者,整理的高频面试题文档,围绕Java核心高频考点系统梳理了Tomcat运行原理、Spring IOC/DI与AOP、JVM运行机制、包装类与基本数据类型区别、Shiro权限标签、Servlet生命周期以及Java作用域七个主题展开,每个主题均以问答方式呈现,便于快速回顾核心概念与常见追问。压缩包整体仅1个docx文件,约37KB,轻量便携,可在Word中直接阅读、标注或打印复习,适合碎片化时间反复翻阅。内容兼顾原理拆解与差异对比,比如Tomcat通过main启动后监听端口并处理请求的过程、Spring通过IOC与DI由容器管理依赖实现解耦、JVM经类加载和解释执行达成跨平台、包装类对象与基本类型存储位置差异、Shiro标签权限判定等,能帮助读者在短时间内厘清易混点、建立知识框架。目前已有543人学习浏览,适合作为面试前冲刺的浓缩笔记。

1. 2025最新版Java面试题:这份题库不是背答案,是帮你把原理串成链路

面试官问「Tomcat 运行原理」,很多人张口就答「8080 端口」。真正想听的是一整条 Servlet 容器链路。这份 2025 最新版 Java 面试题把链路拆成七道题,从 Tomcat、Servlet 生命周期一路讲到 Spring、JVM 和 Shiro 标签,适合正在准备校招和社招的 Java 后端。文档不长,但每道题都踩在面试官追问的点上,而且答案给的是「原理描述」而不是「一句话结论」。接下来按七个考点拆开讲,哪道题能答到第几层、哪里容易翻车,一次性说清。

2. Tomcat 运行原理与 Servlet 生命周期:一条链路串起启动、请求与销毁

2.1 Tomcat 的五步启动模型:从 main 方法到动态类加载

Tomcat 不是靠魔法跑起来的,它本质上是一个 Java 程序。文档里把启动过程拆成五步:创建 Server 实例、监听端口、抓取请求、动态加载类、返回结果。这五步在面试时可以当成一个「故事线」来讲,面试官听到你按顺序走,就知道你对容器模型有整体认知,而不是零散背了几个端口号。

第一步创建 Server。Tomcat 的 Bootstrap 类里有 main 方法,启动后创建 Server 实例,Server 再聚合 Service、Connector、Engine 等组件,这就是文档说的「需要 main 方法启动,创建实例 server」。第二步监听端口。Tomcat 通过 Connector 监听指定端口,HTTP 请求到达后交给容器处理。文档列的 8005、8043、8080 里,8005 是运维关闭端口,8080 是默认 HTTP 连接器端口;8043 在日常生产里很少见,常见 HTTPS 端口是 8443,看到 8043 别照搬进生产配置。

注意:8005 端口只接收 Shutdown 指令,不处理业务请求;8080 才是 HTTP 连接器。面试时说「Tomcat 监听 8080 端口」没问题,补一句「8005 是关闭端口」能加分。

server.xml 里对应配置长这样:

<Server port="8005" shutdown="SHUTDOWN"> <Service name="Catalina"> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> </Service> </Server>

这段配置里,port 是监听端口,connectionTimeout 控制连接超时时间,redirectPort 是收到 HTTPS 请求时的转发端口。启动后 Tomcat 会持续抓取 8080 端口上的请求,拿到请求行、请求头和请求体,再根据 URL 和方法决定交给哪个 Servlet——这就是第三步。

第四步动态加载类。请求到了容器内部,容器按 URL 匹配映射关系找到对应的 Servlet 类,如果类没加载过就反射创建实例,再调用实例上的方法,对应文档说的「动态地加载方法所在的类,完成类的实例化」。第五步把执行结果写成响应,可以是 JSP 渲染后的 HTML,也可以是 JSON/XML 字符串。面试时把这五步讲完,再补一句「整个过程里容器负责线程管理和实例复用,开发者写的是业务逻辑」,就能自然接上生命周期。

2.2 Servlet 生命周期:四个阶段对应哪些方法

Servlet 生命周期文档里分了四个阶段:加载和实例化、初始化、请求处理、服务终止。对应关系如下:实例化由容器构造对象,初始化走 init(),请求处理走 service(),销毁走 destroy()。写一个标准 Servlet 就能把这些方法的位置看干净:

public class DemoServlet extends HttpServlet { public DemoServlet() { System.out.println("1. 实例化:容器创建 Servlet 实例"); } @Override public void init() { System.out.println("2. 初始化:init() 只执行一次"); } @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("3. 请求处理:service() 按请求类型分发"); super.service(req, resp); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.getWriter().write("ok"); } @Override public void destroy() { System.out.println("4. 服务终止:容器关闭时释放实例"); } }

这段代码的要点有三个。第一,实例化和 init 是两次调用,实例是构造出来的,init 是随后调用的,面试时别说成一个。第二,service() 内部按 HTTP 方法分发,GET 走 doGet,POST 走 doPost,这是 HttpServlet 帮开发者做好的事。第三,destroy() 只在容器关闭或应用卸载时触发,单次请求处理完不会走 destroy,很多人在这点上想当然。

补充一个文档没写但高频追问的点:不想等第一次请求才实例化,可以在 web.xml 里配 load-on-startup,让容器启动时就把 Servlet 建好并执行 init:

<servlet> <servlet-name>demo</servlet-name> <servlet-class>com.example.DemoServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet>

load-on-startup 的值是启动顺序,数字越小越先加载。配了这个,容器一启动就执行实例化和 init,适合初始化连接池、预热缓存之类的场景。

2.3 追问高发区:单实例多线程与线程安全

生命周期讲完,面试官大概率追问一句:「Servlet 是单例还是多例?并发请求会怎样?」答案是单实例多线程。容器只维护一个 Servlet 实例,每个请求由一个独立线程调用同一个 service()。所以 Servlet 里的成员变量是被多个线程共享的,如果往成员变量里写数据不做同步,就会出并发问题。

这道题把 Tomcat 原理和并发两个考点连在一起,回答时给一个结论加一个例子:结论是「实例单例、调用多线程」,例子是「在 doGet 里直接给成员变量赋值,两个请求同时进来可能互相覆盖」。本地变量不存在这个问题,因为它们每个线程各有一份。能补一句「所以 Servlet 字段要避免可变状态」就更完整了。

3. Spring 框架三件套:IOC、DI 与 AOP 的面试应答框架

Spring 在面试里的出现率接近百分之百,它不问你知不知道这个词,而是问你能不能讲清楚「容器到底帮你做了什么」。题库的答案只有三句话,但面试官眼里这是三个独立考点,需要分别展开:IOC 讲设计思想,DI 讲实现方式,AOP 讲横切逻辑的落地。下面按这个顺序拆。

3.1 IOC 控制反转:什么被「反转」了

IOC 文档的定义是「把组件间的依赖关系从程序内部转变到外部来管理」,用大白话说:以前对象自己 new 依赖对象,现在把创建权交给容器,你需要什么,容器注入什么。

常见做法是先讲「反转前」,再讲「反转后」。反转前是传统代码,Service 里手动 new DAO 实现类;反转后是 Service 只暴露依赖接口,由容器在启动时装配。讲完对比补一句「反转的是对象创建权和依赖装配权」,这句话是得分点。

对应的容器配置长这样:

<bean id="userDao" class="com.example.dao.impl.UserDaoImpl" /> <bean id="userService" class="com.example.service.UserService"> <property name="userDao" ref="userDao" /> </bean>

第一行声明 DAO 实现类,第二行声明 Service 并把 userDao 通过 setter 注入进去。容器读取这段配置后,会创建两个 Bean 并按 ref 建立关系。面试时能画出「容器 — 创建 — 装配」这个三角关系,比背定义有用。再补一句「开发者不再控制 new 的时机,主动权交到了容器手里」,IOC 这题就答圆了。

3.2 依赖注入的三种方式:构造器、setter、字段

DI 是把「以参数或其它形式注入依赖」落到代码层面的机制。常见的有三种注入方式,写法不同,面试建议把三种都说一遍,显得体系完整。

构造器注入是最推荐的,Spring 官方也偏向它。优点是依赖在创建时就固定,Bean 不可变,且不会出现「依赖为 null 还继续跑」的空指针现场:

@Service public class OrderService { private final OrderDao orderDao; @Autowired public OrderService(OrderDao orderDao) { this.orderDao = orderDao; } }

setter 注入允许 Bean 创建后再改依赖,适合可选的、运行期可能替换的实现。字段注入最简洁,但把依赖直接当成私有字段,外部拿不到,难以做不可变约束,测试时还得靠反射或 @Mockito 处理:

// setter 注入:容器调 setOrderDao 完成装配 @Autowired public void setOrderDao(OrderDao orderDao) { this.orderDao = orderDao; } // 字段注入:简洁但测试不友好 @Autowired private OrderDao orderDao;

说明:第一种走构造方法,参数是 final,Spring 用构造器自动装配;第二种走 setter;第三种直接打在字段上。三种方式的共同点是依赖注入都由容器运行期决定,区别在装配时机和外部可见性。面试时不用表态谁绝对好,说一句「优先构造器,可选项用 setter,字段注入少用」就够了。

3.3 AOP:横切逻辑抽离与两种代理机制

AOP 解决的是「日志、事务、安全」这些横切逻辑反复出现在业务代码里的问题。文档的说法是把这些逻辑抽成独立模块,在指定位置插入。Spring AOP 的落地方式是:定义一个切面类,用注解声明切入点和通知。

@Aspect @Component public class LogAspect { // execution 定义切入点:service 包下所有类的所有方法 @Before("execution(* com.example.service.*.*(..))") public void beforeLog(JoinPoint joinPoint) { String method = joinPoint.getSignature().getName(); System.out.println("[AOP] 调用前:" + method); } @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result") public void afterLog(Object result) { System.out.println("[AOP] 返回结果:" + result); } }

execution 表达式里,第一个 * 是返回值通配,com.example.service.* 是包,第二个 * 是类名通配,第三个 * 是方法名通配,(..) 是任意参数。@Before 在目标方法执行前插入逻辑,@AfterReturning 在正常返回后插入。AOP 能生效的底层是代理:接口实现类走 JDK 动态代理,没有接口的类走 CGLIB 子类代理,面试被问到「JDK 代理和 CGLIB 区别」时,答「前者要求目标类有接口,后者靠继承生成子类」就能过关。

另外,事务处理也是 AOP 的典型应用,Spring 的 @Transactional 底层就是事务切面。面试被问事务传播行为时,提到「这类注解是由 AOP 代理拦截方法调用实现的」,比只背传播级别高一层,因为把注解和容器机制串起来了。

4. JVM 运行原理与 Java 类型体系:从字节码到作用域的完整一条线

JVM 在面试里的定位很特殊:它不是一个孤立考点,而是和「跨平台」「内存」「变量生命周期」全部挂钩。题库给了三条主干,拆开看其实是一条因果链:源码从字节码开始,经类加载进入虚拟机的类型体系,而类型体系又决定变量存放在哪里、存活多长时间。按这个顺序讲,面试官不用追问你就能把链路说全。

4.1 JVM 运行三步走:编译、类加载、解释执行

Java 的跨平台靠的是字节码在 JVM 里解释执行,而不是直接编译成机器码。文档的三步流程是:源码编译字节码、类加载器加载到虚拟机内存、解释器翻译成平台机器码。三步各有各的追问点。

第一步编译。javac 把 .java 编译成 .class,里面是字节码指令。第二步类加载。类加载器按「双亲委派」逐级加载,加载后字节码被放入方法区,堆里生成对应的 Class 对象。第三步解释执行。JVM 解释器逐条翻译字节码指令,热点代码会被 JIT 编译成本地代码提速。回答时强调「一次编译,到处运行」的前提是每个平台都有对应的 JVM 实现。

可以用命令行直观感受类加载过程:

javac StackDemo.java java -verbose:class StackDemo

第一行编译得到 .class 文件。第二行运行并打印类加载日志,能看到 java.lang.String、java.lang.System 这些核心类在启动早期就被加载。面试时提到这个命令,等于告诉面试官你实际观察过类加载的过程,不是只背概念。

4.2 包装类与基本类型:六点差异,其中一条是原题库的易错结论

文档列了四条,我在拆题时逐条查证,其中第一条容易误导:原文说「包装类比基本数据类型占空间少」,这个结论与实际相反。包装类对象要存储对象头、引用字段等元数据,Integer 实际占的内存比 int 大得多,拿一段代码验证:

public class BoxDemo { public static void main(String[] args) { int i = 10; // 栈上的原始 int,占 4 字节 Integer n = Integer.valueOf(i); // 堆上的对象,含对象头,远大于 4 字节 System.out.println(n == i); // n 自动拆箱后比较值 } }

这段代码里 int 直接存放在栈帧,Integer 是通过 valueOf 装箱出来的堆对象。实际差异归纳如下:

第一,空间上基本类型更省,包装类有对象头开销。第二,存储位置上基本类型局部变量在栈里,包装类对象多半在堆里,但局部包装类型的引用变量本身也在栈上,别把「对象在堆」说成「所有包装类都在堆」。第三,包装类是对象,有 equals、compareTo 等方法,基本类型没有。第四,泛型只能用包装类,int 写不进 List。第五,包装类有缓存,Integer 默认缓存 -128 到 127。第六,包装类参与运算会自动拆箱,拆箱空引用会抛 NullPointerException。

这六点正好覆盖包装类方向全部高频追问。原文前三条的含义都在前三点里,第四点泛型限制单独成立。我当时在这个结论上栽过跟头,后来凡是面到包装类,都先答「空间上基本类型更省」,再从对象、泛型、缓存、拆箱四个方向补,没有再翻车。

4.3 Java 作用域:四种变量范围与默认值差异

文档最后一道题是作用域,四种位置分别是局部变量、实例变量、类变量、方法参数。用一段代码一次看全:

public class ScopeDemo { private static String classVar = "类变量"; // 类变量:属于 Class,全体实例共享 private String instanceVar; // 实例变量:每个对象一份 public void say(String paramVar) { // 方法参数:形参 String localVar = "局部变量"; // 局部变量:方法内的临时变量 System.out.println(paramVar + localVar); System.out.println(instanceVar); // 实例变量有默认值 null } }

四种变量的核心区别有两条。第一是生命周期:局部变量随方法调用进栈、方法结束出栈;方法参数与局部变量相似但由调用方传值;实例变量随对象创建和回收;类变量随类的加载和卸载,应用不死它不灭。第二是初始值:局部变量和参数必须先赋值才能用,实例变量和类变量有默认值,像 String 默认 null,int 默认 0。写代码时「局部变量没初始化直接读」在编译期就报错,这是常见的初学者翻车点。

面试时遇到作用域题,先答四种变量,再补一句「类变量用 static 修饰,所有实例共享一份」,就完整了。

5. Shiro 权限标签实战与避坑清单:guest、user、authenticated 的边界

5.1 五个标签的语义边界:最容易记混的三兄弟

文档给了五个常用标签,guest、user、authenticated 是权限状态判断,principal 是输出信息,hasRole 和 hasPermission 是授权判断。三个状态标签是重灾区,很多人记混。核心区别是「登录方式」:

标签判断条件典型场景
shiro:guest当前用户未登录未登录时的引导页
shiro:user已登录,或通过 Remember Me 记住已登录但可能只被记住
shiro:authenticated必须走完认证流程,不包括记住我敏感操作、个人中心

guest 和 user 是互补的:guest 表示没登录,user 表示登录过或有记住我凭证。authenticated 和 user 的差异最容易被追问:勾选「记住我」后再访问,shiro:user 会显示内容,shiro:authenticated 不会,因为 Remember Me 只恢复了身份凭证,没重新走认证流程。页面里做权限控制时,普通页面用 user,下单改密这类敏感操作必须用 authenticated。

5.2 JSP 里的标签接入:从依赖到页面判断

要让标签生效,先确保项目里引入 Shiro 的 Web 集成依赖并配置过滤器,常见依赖如下:

<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.13.0</version> </dependency>

再在 web.xml 里把 ShiroFilter 挂到应用入口,让请求先过过滤器再做权限判断。过滤器链的常见配置在 shiro.ini 里长这样:

[main] authc.loginUrl = /login [urls] /order/** = authc /admin/** = authc, roles[admin] /export/** = authc, perms[order:export]

URL 映射以斜杠开头,等号后面是过滤器链。roles[admin] 表示要求 admin 角色,perms[order:export] 表示要求对应权限字符串。这个配置决定了后面 JSP 标签能不能拿到权限信息,面试时说清「标签只是展示层,真正拦截在过滤器链」即可。

JSP 页面的用法如下:

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <shiro:guest>未登录访客可见</shiro:guest> <shiro:user>已登录或记住我可见</shiro:user> <shiro:authenticated>真正认证过的用户可见</shiro:authenticated> <shiro:hasRole name="admin">有 admin 角色可见</shiro:hasRole> <shiro:hasPermission name="order:export">有导出订单权限可见</shiro:hasPermission> <shiro:principal property="username"/>

最后一行 principal 标签可以直接输出当前登录用户名,property 指定输出字段,在导航栏上很常用。hasRole 和 hasPermission 的 name 值要和 Shiro 配置里的权限字符串严格对应,字符串写错,标签就静默不显示内容。

5.3 三条踩坑记录:现象、原因、解决

项目里用 Shiro 标签,最常翻车的就是下面三个,都是实际项目中交过学费换来的血泪经验:

坑一:记住我用户被 user 标签放行。

  • 现象:用户勾选「记住我」后,关闭浏览器再打开,个人中心页用 shiro:user 判断,页面正常显示。
  • 原因:user 标签的语义包含「已记住用户」,它不等同于重新认证。敏感页面用 user 标签等于把 Remember Me 当成完全登录。
  • 解决:敏感页面把 shiro:user 换成 shiro:authenticated,心里记住「authenticated 才算真登录」。从那以后凡是涉及个人数据、订单、支付的页面,我一律只信 authenticated。

坑二:hasPermission 在 JSP 里永远不显示内容。

  • 现象:权限标签包住的内容一直不渲染,换成 hasRole 又能生效。
  • 原因:hasPermission 里的权限字符串和 Realm 里配置的权限标识对不上,Shiro 对大小写和冒号分隔是严格匹配的。
  • 解决:先开 debug 日志看 Realm 返回的权限列表,再核对标签里的 name。常见做法是统一维护一个权限字典,前端写标签、后端配权限都从同一张表抄,避免手打出错。

坑三:页面直接输出一段带尖括号的标签文本。

  • 现象:JSP 页面上出现原始标签代码,没有被解析。
  • 原因:常见两类,要么 taglib 的 URI 写错,要么 web.xml 里 ShiroFilter 没有映射到当前 URL,标签没被容器识别。
  • 解决:先检查页面头部 taglib 的 uri 是否和依赖里的 tld 一致,再检查过滤器的 url-pattern。排查顺序上,我一般先看 taglib uri,再看过滤器映射,最后开 debug 日志核对权限字符串,三步走完基本都能定位。

6. 把原理变成面试答案:两分钟口述模板与追问防御清单

题库里的题目是知识,面试官要的是表达。我习惯把每道原理题组织成一个两分钟模板:先给结论,再讲顺序,最后补一个易错点。

以 Tomcat 为例,两分钟这样说:先给结论「Tomcat 是 Servlet 容器,启动时创建 Server,监听端口,请求到来时按 URL 映射到 Servlet 实例,执行方法并返回」。再按顺序讲五步,重点落在「类加载和实例化」上。最后补一句易错点「Servlet 是单实例多线程,成员变量要注意并发安全」。整个回答既有骨架,又有别人容易忽略的细节。

追问防御更重要。面试官听完原理,通常从下面几个方向继续打:

追问方向应答要点
Tomcat 端口异常先看 8005 与 8080 是否冲突,别只看业务端口
IoC 和 DI 有什么区别IoC 是设计思想,DI 是它的实现方式之一
AOP 代理怎么选有接口走 JDK 代理,没接口走 CGLIB
包装类为什么能用泛型泛型约束引用类型,自动装箱做适配
记住我算不算登录算身份恢复,不算完整认证

面试前一天,我会把题库七个考点口头过一遍:每个考点先花 30 秒说结论,再花 60 秒走流程,最后 30 秒讲一个坑。整个过程不碰文档,讲不出来就在题目前面做记号,回去重点补。这套自检脚本帮我避开了很多「看着会、张嘴忘」的场面,建议你也拿题库按这个流程过一遍。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询