☰
Java核心原理图谱:Tomcat/Spring/JVM/包装类全链路解析
2026/10/24 0:21:11 网站建设 项目流程

简介:本资源是一份面向Java中高级开发者与面试求职者的系统性复习资料,聚焦Web开发核心组件与JVM底层原理,助力高效备战技术面试。内容涵盖Tomcat运行机制、Spring框架IOC/AOP/DI工作原理、JVM字节码加载与执行流程、基本类型与包装类本质区别、Shiro权限标签实战用法、Servlet完整生命周期,以及Java作用域层级与内存分布等七大高频考点,全部基于2025年主流技术栈更新整理。资源为单文件Word文档(.docx),共1个文件,大小仅37KB,轻量易读,结构清晰,每部分均含原理简述与关键要点提炼,适合作为速查笔记或面试前突击提纲。目前已有544人学习下载,内容精炼准确,无冗余示例,直击面试官常问的‘为什么’与‘怎么运作’类深度问题,帮助读者建立技术纵深理解而非碎片化记忆。

1. Java面试题精选(2025最新版)到底是什么:一份帮你把“背题”变成“真懂”的通关地图

你有没有过这种经历:打开一份Java面试题文档,密密麻麻全是“Spring怎么工作?”“JVM内存模型?”“Tomcat端口是啥?”——字都认识,连起来却像在读天书?更糟的是,背了十遍“IOC是控制反转”,一到面试官问“那如果不用Spring,你自己手写一个简易容器,第一步该干啥?”,脑子瞬间空白。这不是你记性差,而是大多数题库只给了“答案的壳”,没给你“理解的根”。这份《Java面试题精选(2025最新版)》不是又一本让你死记硬背的PDF,它是一张可执行的认知地图:每一道题背后,都锚定一个真实运行机制(比如Tomcat监听8080端口时,底层到底在操作系统里做了什么系统调用?)、一个设计哲学(比如Spring为什么非要把new对象这件事抢过来自己干?)、甚至一个踩坑现场(比如为什么包装类Integer a=127; Integer b=127; 用==比较是true,但a=128就变false?)。它不教你“标准答案”,它逼你回到代码跑起来那一刻——服务器怎么听请求、虚拟机怎么认字节码、容器怎么把对象塞进你的service方法里。搞懂这些,你答的就不是题,而是整个Java世界的运转逻辑。


2. Tomcat运行原理:一个Web服务器如何从“开机”到“接单”的全流程拆解

Tomcat不是黑匣子,它是一套有血有肉的Java程序。它的启动和响应过程,就是一次教科书级的“网络服务实现课”。理解它,你就看懂了所有Java Web应用的起点。

2.1 启动:从main方法开始的“自我组装”

很多人以为Tomcat是独立于Java的“服务器软件”,其实它本身就是一个用Java写的、带main方法的普通程序。当你双击startup.bat或执行java -jar catalina.jar时,JVM会加载Tomcat的启动类(通常是org.apache.catalina.startup.Bootstrap),并调用其main方法。这个main方法干的第一件事,就是创建一个Server实例——这是整个Tomcat架构的顶层容器。你可以把它想象成一家快递公司的总部:Server是总调度中心,下面管着Service(比如“华东区配送服务”),Service再管着Connector(快递员接单口)和Container(分拣中心)。没有这个main方法启动的Server,Tomcat就只是硬盘上一堆.class文件,永远不会“活”过来。

// 简化示意:Tomcat启动的核心逻辑链 public class Bootstrap { public static void main(String[] args) { // 1. 创建Server实例(总部) Server server = new StandardServer(); // 2. 给Server配置Service(华东区服务) Service service = new StandardService(); server.addService(service); // 3. 给Service配置Connector(8080接单口)和Engine(分拣中心) Connector connector = new Http11NioProtocol(); connector.setPort(8080); service.addConnector(connector); Engine engine = new StandardEngine(); service.setContainer(engine); // 4. 启动!总部发号施令,所有部件开始运转 server.initialize(); server.start(); } }

提示:这段代码不是让你去改Tomcat源码,而是让你看清——所谓“启动Tomcat”,本质就是用Java代码把一堆对象(Server、Service、Connector、Engine)按规则组装起来,并调用start()方法让它们进入工作状态。每个对象都有明确职责,没有魔法。

2.2 监听:端口不是数字,而是操作系统给你的“收件箱”

Tomcat要对外提供服务,必须告诉操作系统:“请把发往本机8080端口的所有网络包,都转给我处理。”这背后是Java NIO(非阻塞I/O)在发力。当Connector启动时,它会调用Java的ServerSocketChannel.open(),然后绑定到8080端口。操作系统内核会为这个端口创建一个特殊的“队列”(即accept队列),所有客户端(比如浏览器)发来的TCP连接请求(SYN包),都会被内核先收下,排在这个队列里。Tomcat的Acceptor线程就像个门卫,不断轮询这个队列,一旦发现有新请求,就立刻“取走”(accept),并交给Worker线程池去处理真正的HTTP请求。8005端口(shutdown端口)和8043端口(HTTPS端口)同理,只是各自负责不同类型的“收件箱”。

2.3 请求处理:从URL到Java方法的“精准投递”

用户在浏览器输入http://localhost:8080/user/list,这个请求到达Tomcat后,绝不是随机扔给某个类处理。整个过程像快递分拣:

  • 第一步:解析URL路径。Tomcat根据/user/list,结合web.xml或注解(@WebServlet("/user/list")),找到对应的Servlet类(比如UserServlet)。
  • 第二步:动态加载与实例化。如果UserServlet类还没被JVM加载,Tomcat的WebAppClassLoader会从WEB-INF/classes或WEB-INF/lib下的jar包里,把UserServlet.class字节码读进来,用Class.forName("com.example.UserServlet").newInstance()创建实例(注意:现代Tomcat多用反射+构造器,而非简单newInstance)。
  • 第三步:调用正确的方法。Tomcat分析HTTP方法(GET/POST),调用UserServlet的doGet()或doPost()方法,并把封装了请求头、参数、Body的HttpServletRequest和用于写响应的HttpServletResponse对象作为参数传进去。
  • 第四步:返回结果。Servlet方法执行完毕,把HTML字符串、JSON数据或跳转指令写入response,Tomcat再通过Socket把响应原路发回浏览器。

这个过程之所以能“动态”,核心在于Java的反射机制和类加载器隔离。每个Web应用(war包)都有自己的ClassLoader,互不干扰,所以你能同时部署多个不同版本的Spring应用。

2.4 常见误区:Tomcat、Servlet、HTTP服务器,谁是谁的爹?

易混点为什么混怎么区分
认为Tomcat就是Servlet因为常一起出现,且Tomcat最出名的功能是跑ServletServlet是一套规范(接口,如javax.servlet.Servlet),定义了init()、service()等方法;Tomcat是这个规范的一个具体实现(即Servlet容器),它负责创建Servlet实例、调用其方法。好比“快递员”是职业规范,“顺丰小哥”是具体公司雇的人。
以为8080端口是Tomcat“独占”的看到报错“Address already in use: JVM_Bind”就慌了端口是操作系统资源,任何程序(包括另一个Tomcat、Node.js、甚至QQ)都能监听8080。Tomcat只是申请使用它。冲突时,要么改Tomcat端口(conf/server.xml里改<Connector port="8080" />),要么关掉占端口的程序。
混淆Connector和Container名字都带“con”,以为是一个东西Connector是入口(接单员),负责网络通信(HTTP/HTTPS/AJP协议);Container是核心处理引擎(分拣+派送),包含Engine(全局)、Host(虚拟主机)、Context(Web应用)、Wrapper(Servlet实例)四级结构。一个Tomcat可以配多个Connector(如8080 HTTP + 8443 HTTPS),但只有一个顶级Container。

3. Spring工作原理:为什么它说“别自己new对象”,而要把控制权交出去?

Spring不是“让Java更好写”的工具,它是对传统面向对象编程的一次“权力重构”。它把对象创建、依赖组装、功能增强这些事,从程序员的手上,夺过来,交给一个叫“IoC容器”的管家统一打理。理解这点,才能明白为什么面试必问IOC、DI、AOP。

3.1 IOC(控制反转):从“我创建你”到“你安排我”

传统代码里,类A要用类B,得自己写B b = new B();——A完全掌控B的生命周期(什么时候生、什么时候死)。这导致A和B强耦合:换B的实现,就得改A的代码。Spring的IOC说:“别忙,这事我来管。”你只需声明“我需要B”,Spring容器在启动时,会自动创建B的实例,并“塞”给A。这个“塞”的动作,就是DI(依赖注入)。控制权从“类自己”反转到了“外部容器”,故称“反转”。

// 传统方式:A主动创建B,强耦合 public class OrderService { private PaymentService paymentService = new AlipayPaymentService(); // 硬编码,无法替换 public void pay(Order order) { paymentService.execute(order); } } // Spring方式:A只声明需求,容器负责供给 @Service public class OrderService { @Autowired // 这就是“我需要PaymentService”的声明 private PaymentService paymentService; // 容器会在运行时把合适的实例赋值给它 public void pay(Order order) { paymentService.execute(order); // 具体是Alipay还是Wechat,由容器配置决定 } }

注意:@Autowired不是魔法。Spring启动时,会扫描所有加了@Service、@Component等注解的类,把它们的BeanDefinition(定义信息)注册到容器中。然后,当创建OrderService Bean时,容器发现它依赖PaymentService,就去容器里找所有PaymentService类型的Bean(可能有多个,需用@Qualifier指定),找到后,通过反射把实例注入到OrderService的paymentService字段里。

3.2 DI(依赖注入):三种注入方式,哪种才是生产环境首选?

Spring支持三种DI方式,但实际项目中,构造器注入(Constructor Injection)是官方推荐且最安全的。

  • 字段注入(@Autowired on field):最简洁,但破坏了类的封装性(字段本应private,却被容器强行修改),且无法在new对象时传入依赖,单元测试困难。
  • Setter注入(@Autowired on setter):比字段注入好,但setter方法可能被外部随意调用,导致对象状态不一致。
  • 构造器注入(@Autowired on constructor):在对象创建时,所有必需依赖就必须提供,保证了对象的不可变性和完整性。Spring 4.3+对单构造器类自动注入,无需写@Autowired。
@Service public class OrderService { private final PaymentService paymentService; private final NotificationService notificationService; // 构造器注入:强制要求所有依赖在创建时就位 public OrderService(PaymentService paymentService, NotificationService notificationService) { this.paymentService = paymentService; this.notificationService = notificationService; } public void pay(Order order) { paymentService.execute(order); notificationService.sendSuccess(order); } }

3.3 AOP(面向切面):把“横切关注点”从代码里“剪”出来贴回去

日志、事务、权限检查、性能监控……这些功能,几乎每个业务方法都要加,但它们和核心业务逻辑(比如“扣库存”、“发短信”)毫无关系。如果每个方法里都写一遍log.info("start..."); try{...} catch{...} log.info("end");,代码会变得臃肿、重复、难以维护。AOP就是一把“剪刀”:它允许你定义一个“切面”(Aspect),比如@Around("execution(* com.example.service..*.*(..))"),意思是“在所有service包下的所有方法执行前后,都执行我的逻辑”。然后,Spring在运行时,用动态代理(JDK Proxy或CGLIB)生成一个代理对象,在调用目标方法前,先执行你的日志逻辑,再执行目标方法,最后再执行收尾逻辑。业务代码里,干干净净只有“扣库存”,日志、事务等“横切”功能,被织入(Weave)到字节码里。

3.4 常见误区:IOC容器、BeanFactory、ApplicationContext,谁管谁?

易混点为什么混怎么区分
以为BeanFactory就是Spring容器文档里常提BeanFactory,以为它就是全部BeanFactory是Spring最顶层的IOC容器接口,定义了getBean()等基础方法;ApplicationContext是它的子接口,提供了更多企业级功能(事件发布、国际化、AOP集成、Web环境支持)。开发中几乎只用ApplicationContext(如AnnotationConfigApplicationContext),BeanFactory只在极简场景或框架内部使用。
认为@Autowired只能注入Spring管理的Bean看到注入失败就怀疑配置错了@Autowired默认按类型(byType)匹配。如果容器中有多个同类型Bean(如两个PaymentService实现),必须用@Qualifier("alipayService")指定名字,否则报错。它不能注入new出来的普通对象,因为那些不在容器管理范围内。
把AOP和装饰器模式混为一谈都是“增强”功能,感觉差不多装饰器是编译期静态的(你手动new一个new LoggingPaymentService(new AlipayPaymentService())),AOP是运行期动态的(Spring自动生成代理类,你代码里看不到代理)。AOP更灵活,无需修改原有类,且能统一管理所有匹配方法。

4. JVM运行原理:Java字节码如何从“纸面文字”变成“CPU指令”的全程直播

JVM是Java的“操作系统”,它让Java代码摆脱了硬件束缚,实现了“一次编写,到处运行”。但这句话背后,是一整套精密的翻译与执行流水线。搞懂它,你才能真正理解为什么String是不可变的、GC何时会触发、以及线上OOM问题该怎么查。

4.1 编译:javac不是翻译,是“转录”

Java源文件(.java)经过javac编译,生成的不是机器码,而是字节码(.class文件)。字节码是一种中间表示(IR),它不针对任何具体CPU,而是为JVM这台“假想计算机”设计的指令集。比如int a = 1 + 2;,javac会编译成三条字节码指令:iconst_1(把1压入操作数栈)、iconst_2(把2压入栈)、iadd(弹出栈顶两数相加,结果压回栈)。字节码是平台无关的,但它也是严格校验过的:JVM加载时会做“字节码验证”,确保指令不会越界访问数组、不会把String当成int用——这是Java安全性的第一道防线。

4.2 类加载:双亲委派模型,一场关于“信任”的接力赛

JVM不会一上来就把所有类都加载进内存。它采用双亲委派模型(Parent Delegation Model):当一个类加载器收到加载请求,它首先不会自己去加载,而是把请求委派给父类加载器。只有当父加载器无法完成加载(比如在它的路径下找不到该类),子加载器才会尝试自己加载。这个模型的顶级是Bootstrap ClassLoader(加载rt.jar里的核心类,如java.lang.Object),然后是Extension ClassLoader(加载jre/lib/ext下的扩展类),最后是Application ClassLoader(加载classpath下的应用类)。这样做的核心目的是安全与稳定:确保java.lang.String永远由Bootstrap加载,防止有人自己写个恶意的String类,通过自定义类加载器偷偷替换掉它。

4.3 运行时数据区:内存不是铁板一块,而是分工明确的“行政区”

JVM内存被划分为几个逻辑区域,各司其职:

  • 方法区(Method Area):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。JDK 8后,它被元空间(Metaspace)取代,直接使用本地内存(Native Memory),不再受-XX:MaxPermSize限制。
  • 堆(Heap):所有对象实例和数组的分配地,是GC的主要战场。新生代(Eden+Survivor)、老年代(Old Gen)的划分,是为了适配“大部分对象朝生暮死”的现实。
  • 虚拟机栈(JVM Stacks):每个线程私有,存储局部变量、操作数栈、动态链接、方法出口等。一个方法对应一个栈帧(Stack Frame),方法调用=压栈,方法返回=弹栈。
  • 本地方法栈(Native Method Stacks):为Native方法(如Object.wait()底层调用的C函数)服务。
  • 程序计数器(Program Counter Register):记录当前线程所执行的字节码行号,是唯一一个没有OOM风险的区域。

4.4 执行引擎:解释器与JIT编译器的“双模驱动”

字节码不能直接被CPU执行,必须翻译。JVM有两种翻译模式:

  • 解释执行(Interpreter):逐条读取字节码,翻译成机器码并执行。启动快,但慢。
  • 即时编译(JIT, Just-In-Time):当某个方法被频繁调用(达到阈值,如HotSpot的10000次),JVM会将它的字节码编译成本地机器码,并缓存起来。下次再调用,就直接执行高速的机器码,跳过解释。这就是为什么Java程序“越跑越快”。JIT是JVM的智慧大脑,它会做逃逸分析、锁消除、方法内联等深度优化。

4.5 常见误区:堆内存、栈内存、方法区,到底谁存什么?

易混点为什么混怎么区分
认为“基本类型存在栈,引用类型存在堆”是绝对真理教科书常这么讲,但忽略了逃逸分析这是默认情况,但JVM的逃逸分析可能让对象“栈上分配”。如果一个新对象在其方法内创建,且从未“逃逸”出该方法(即没被返回、没被赋值给静态变量、没被传给其他线程),JIT编译器可能直接把它分配在栈上,方法结束自动回收,省去GC开销。所以,对象不一定都在堆上。
把String常量池(String Pool)当成堆的一部分常量池确实在堆里(JDK 7+),但行为特殊String s = "hello";中的"hello"字面量,会被放入字符串常量池(堆中一个特殊区域)。而new String("hello")会创建两个对象:一个在常量池(如果不存在),一个在堆上(new出来的)。==比较的是引用地址,所以s1=="hello"为true,但s2==s1为false。
以为GC只清理堆内存GC名字叫“垃圾收集”,自然想到堆GC主要针对堆和方法区(元空间)。堆里对象死亡由可达性分析(GC Roots)判定;元空间里,已加载的类如果不再被任何对象引用,且满足条件(如-XX:+ClassUnloadingWithConcurrentMark),也会被卸载。栈和程序计数器是线程私有,随线程结束自动销毁,无需GC。

5. Java包装类与基本类型:为什么127==127,但128!=128?一个关于缓存的“玄学”真相

Java为8种基本类型(byte, short, int, long, float, double, char, boolean)提供了对应的包装类(Byte, Short, Integer, Long, Float, Double, Character, Boolean)。它们看起来像“升级版”,但背后的设计哲学和内存行为,藏着无数面试翻车现场。

5.1 内存布局:栈与堆的“身份之别”

基本类型(如int i = 10;)是值类型,变量i本身就在栈上,里面直接存着数字10。而包装类(如Integer j = new Integer(10);)是引用类型,变量j在栈上,但它只是一个指向堆上Integer对象的“地址”。这个对象里,才真正存着那个int值。所以,包装类必然比基本类型占用更多内存:除了值本身,还有对象头(Mark Word、Klass Pointer)、对齐填充等JVM对象开销。

5.2 自动装箱/拆箱:便利背后的“隐形转换”

Java 5引入的自动装箱(Autoboxing)和拆箱(Unboxing),让Integer i = 10;和int j = i;成为可能。但这只是编译器的语法糖。Integer i = 10;在编译后,实际是Integer i = Integer.valueOf(10);;int j = i;则是int j = i.intValue();。关键就在valueOf()方法:

public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }

IntegerCache是一个静态内部类,它在JVM启动时,就预先创建了-128到127(默认范围)之间所有Integer对象,并缓存在一个数组里。所以,Integer a = 127; Integer b = 127;,调用的都是valueOf(127),返回的是同一个缓存对象,a == b自然为true。而Integer c = 128; Integer d = 128;,超出了缓存范围,valueOf()会new Integer(128)两次,创建两个不同的对象,c == d就是false。这根本不是“127很特殊”,而是缓存策略的边界效应。

5.3 泛型与集合:为什么List 不存在,而List 可以?

Java泛型是类型擦除的。编译后,List<Integer>和List<String>都变成了List(原始类型)。这意味着,泛型要求类型参数必须是引用类型,因为只有引用类型才能被擦除成Object。基本类型无法被擦除,所以List<int>在语法上就不合法。所有集合(ArrayList, HashMap等)都只能存包装类。这也是为什么大量使用int循环时,for (int i : list)(list是List<Integer>)会产生大量自动拆箱,成为性能瓶颈。

5.4 常见误区:包装类、基本类型、null,三者混合运算的“雷区”

易混点为什么混怎么区分
认为Integer a = null; int b = a;会得到b=0想当然觉得null会转成默认值这行代码会直接抛出NullPointerException!因为拆箱a.intValue()时,a是null,调用null对象的方法必然空指针。这是新手高频翻车点。安全做法是先判空:if (a != null) b = a;或用Optional.ofNullable(a).orElse(0)。
用==比较两个Integer对象,以为比较的是数值忘了==比较的是引用地址,不是内容如前所述,==只在缓存范围内可靠。要比较数值,必须用equals()或Integer.compare(a, b) == 0。equals()方法内部会先判null,再拆箱比较值,是安全的。
以为Boolean.TRUE和new Boolean(true)是同一个对象Boolean也用了缓存,但只缓存了TRUE和FALSE两个实例Boolean a = true; Boolean b = true;,a == b为true,因为都指向Boolean.TRUE。但Boolean c = new Boolean(true);是新对象,c == a为false。不过,Boolean的valueOf(boolean)方法也返回缓存实例,所以Boolean d = Boolean.valueOf(true);,d == a为true。

6. Shiro权限标签与Servlet生命周期:从“用户登录”到“页面渲染”的全链路权限控制

一个完整的Java Web请求,从浏览器敲下回车,到最终看到页面,中间要穿越Servlet容器、安全框架、MVC层。Shiro的标签和Servlet的生命周期,就是这条链路上最关键的两个“守门人”和“时间刻度”。

6.1 Shiro权限标签:JSP页面上的“实时安检门”

Shiro提供了一组JSP Taglib,让开发者能在视图层(JSP)直接写逻辑,控制哪些内容对谁可见。这不是简单的if-else,而是基于Shiro Subject(当前用户主体)的实时安全上下文判断。

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <html> <body> <!-- 访客可见,登录用户不可见 --> <shiro:guest> <p>欢迎光临!请<a href="/login">登录</a>或<a href="/register">注册</a></p> </shiro:guest> <!-- 已登录用户(含记住我)可见 --> <shiro:user> <p>你好,<shiro:principal property="username"/>!<a href="/logout">退出</a></p> </shiro:user> <!-- 仅认证通过(非记住我)用户可见 --> <shiro:authenticated> <p>您的账户已通过严格认证。</p> </shiro:authenticated> <!-- 拥有admin角色的用户可见 --> <shiro:hasRole name="admin"> <a href="/admin/dashboard">管理员后台</a> </shiro:hasRole> <!-- 拥有user:delete权限的用户可见 --> <shiro:hasPermission name="user:delete"> <button onclick="deleteUser()">删除用户</button> </shiro:hasPermission> </body> </html>

这些标签的底层,是Shiro的SecurityManager在每次请求时,根据当前Subject(从Session或Token中获取)的Principal(身份)和AuthorizationInfo(授权信息),实时调用isPermitted()、hasRole()等方法进行判断。它比在Controller里用@RequiresPermissions注解更细粒度,能控制到按钮、链接、段落级别。

6.2 Servlet生命周期:四阶段,定义了一个Java Web组件的“一生”

Servlet不是一启动就永远在线的“永动机”,它有严格的生命周期,由Servlet容器(如Tomcat)全程管理。理解这四个阶段,你就知道init()里该放什么、destroy()里该清理什么。

阶段触发时机容器行为开发者该做什么关键点
1. 加载与实例化容器启动时(load-on-startup > 0)或首次请求时查找Servlet类,用Class.newInstance()创建实例不要放耗时操作!此时Servlet还未初始化,getServletContext()等方法不可用。实例是单例的,被所有请求线程共享。
2. 初始化(init)实例化后,容器立即调用调用Servlet.init(ServletConfig config),传入配置放一次性初始化工作:读取配置文件、建立数据库连接池、加载缓存数据。config.getInitParameter("xxx")可获取web.xml中<init-param>。init()只执行一次,且必须成功,否则Servlet被标记为不可用。
3. 请求处理(service)每次HTTP请求到达调用Servlet.service(HttpServletRequest req, HttpServletResponse resp),再根据method分发到doGet()/doPost()放核心业务逻辑:解析参数、调用Service层、设置响应。注意线程安全!实例变量(如private List data;)会被多线程并发修改,必须加锁或改用局部变量。service()是线程安全的入口,但Servlet实例本身不是线程安全的。
4. 服务终止(destroy)容器关闭或Servlet被移除时调用Servlet.destroy(),之后不再接受新请求放资源清理工作:关闭数据库连接、注销监听器、清空缓存。保证destroy()快速返回,不要阻塞。destroy()后,Servlet实例可能还在内存,但已不再被容器调度。

6.3 权限控制与生命周期的协同:一次登录请求的完整旅程

假设用户访问/dashboard,这是一个需要admin角色的页面:

  1. 请求抵达:Tomcat的Connector监听到8080端口的HTTP GET请求,解析出URL/dashboard。
  2. 路由匹配:Tomcat根据web.xml或@WebServlet("/dashboard"),找到对应的DashboardServlet。
  3. 实例与初始化:如果DashboardServlet是首次使用,Tomcat创建其实例并调用init()。
  4. 安全过滤(Shiro Filter):在service()执行前,Shiro的FilterChain会拦截请求,调用SecurityManager.checkPermission("admin")。如果用户无此角色,直接重定向到/unauthorized,DashboardServlet.service()根本不会被执行。
  5. Servlet处理:权限通过后,Tomcat调用DashboardServlet.service(),进而调用doGet()。Servlet从req中获取用户信息(可能来自Shiro的SecurityUtils.getSubject().getPrincipal()),查询数据,设置req.setAttribute("data", data)。
  6. JSP渲染:RequestDispatcher.forward()跳转到dashboard.jsp。在JSP中,<shiro:hasRole name="admin">标签再次调用Shiro API,确认当前Subject仍有admin角色,才渲染出管理按钮。
  7. 响应返回:Tomcat将渲染好的HTML通过Socket发回浏览器。

这个过程清晰地展示了:Shiro标签是视图层的“二次安检”,而Servlet生命周期定义了业务代码的“执行节奏”。两者配合,才构成完整的、可落地的权限体系。

6.4 常见误区:Shiro标签、Filter、Interceptor,权限控制的三层防御

易混点为什么混怎么区分
以为<shiro:hasRole>和@RequiresRoles功能一样,只是写法不同都是做角色检查,容易以为可互换@RequiresRoles是Controller层注解,在方法执行前拦截,失败则抛异常(如403);<shiro:hasRole>是View层标签,只控制JSP片段是否渲染,不影响后端逻辑。二者互补:注解做“硬拦截”,标签做“软展示”。
把Shiro Filter和Spring MVC Interceptor搞混都在请求链路上,都可做权限检查Shiro Filter是Servlet规范的Filter,位于整个请求链最外层,对所有请求(包括静态资源)生效;Spring Interceptor是Spring MVC框架的概念,只对DispatcherServlet处理的请求有效(即@Controller映射的路径)。Filter更底层,Interceptor更贴近业务。
认为<shiro:principal>输出的就是用户名标签名直译是“主体”,以为就是nameshiro:principal输出的是Subject.getPrincipal()的返回值,它默认是UsernamePasswordToken.getUsername(),但你可以自定义Realm,让getAuthenticationInfo()返回任意对象(如UserDTO),那么<shiro:principal property="email"/>就能输出邮箱。

从Tomcat监听端口那一刻起,到Shiro标签在JSP里画出一个按钮,整个Java Web世界,就是由这些看似零散的概念,严丝合缝咬合而成的精密齿轮。我曾经在模拟项目X里,为了排查一个“用户登录后首页菜单不显示”的问题,花了整整两天:先是怀疑Shiro配置,查shiro.ini;又怀疑前端JS没加载,抓包看Network;最后才发现,是DashboardServlet的init()方法里,一个数据库连接池初始化失败,导致整个Servlet被容器标记为“不可用”,后续所有请求都被静默忽略——而错误日志被淹没在Tomcat的海量输出里。从那以后,我每次写Servlet,都强制走一遍生命周期 checklist:init()里只做轻量初始化,所有耗时操作丢给异步线程;service()里绝不操作实例变量;destroy()里必须显式关闭所有资源。希望帮到你。

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

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

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

立即咨询