首页 / 资讯中心 / 文章详情

Spring面试25问:IoC原理、Bean生命周期与自动装配全解析

Spring面试25问:IoC原理、Bean生命周期与自动装配全解析 ★ FEATURED ARTICLE
简介经典Spring面试题及答案docx文档包面向Java后端开发人员和准备Spring相关岗位面试的求职者系统梳理Spring框架中的核心概念与高频考点。资源以单个docx文档组织文件大小约88KB携带方便可直接打开阅读或打印复习。文档涵盖25个经典面试问题从Spring框架基础模块、IoC控制反转与依赖注入到BeanFactory与ApplicationContext区别、Bean生命周期与作用域、注解配置、自动装配实现再到构造器注入与setter注入对比以及框架中应用的设计模式等知识点覆盖全面能够帮助读者快速建立Spring知识框架并针对常见面试难点查漏补缺。资源已有403人学习下载适合正在备战面试、希望巩固Spring底层原理的初中级Java开发者使用。1. 备战Spring面试先吃透这份25问清单从IoC原理背到自动装配有一次模拟面试对方突然问“Spring 的单例 bean 是线程安全的吗”我脱口而出“安全”结果被追问了一句“那如果这个 bean 里有一个计数器呢”现场直接卡住。Spring 面试题就是这样——题目看起来都是概念考的全是对边界的理解。这份经典 Spring 面试题及答案清单收集了 25 个高频问答覆盖 IoC/DI 原理、BeanFactory 与 ApplicationContext 的区别、Bean 生命周期、五种作用域、集合注入、自动装配、设计模式等 Java 开发者绕不开的考点。不管你是准备面试还是想系统过一遍 Spring 基础把它当成自查清单来用比自己零散翻文档高效得多。2. IoC与DI先把“谁控制谁”讲清楚再谈容器接口2.1 控制反转与依赖注入两个词一条主线面试官问“什么是控制反转”的时候很多人会直接背“对象交给容器管理”。这句话对但不够。控制反转的本义是程序不再自己控制依赖对象的创建和绑定而是把控制权交给外部的装配器这个装配器在运行时把对象之间的耦合关系绑定好。传统写法里Service 直接 new 一个 DAO编译期就绑死了用了 IoC 之后Service 只声明自己需要一个 DAO具体从哪来、怎么初始化全由容器决定。依赖注入就是控制反转的具体落地方式。Spring 支持三种注入形式构造器注入、setter 方法注入、接口注入。到面试这里我会建议先用一句话点题依赖注入是在编译阶段不知道依赖具体来自哪个类时由外部框架把依赖对象实例化并注入进来的机制。然后再把三种方式列出来构造器注入是在创建时通过构造参数传入setter 注入是对象创建后通过 set 方法赋值接口注入现在已经少用了解即可。下面是一个最简的构造器注入示例先不用 Spring单独看这个模式public class OrderService { private final PaymentClient paymentClient; public OrderService(PaymentClient paymentClient) { this.paymentClient paymentClient; } }这里把 PaymentClient 作为构造参数传入OrderService 内部不再负责创建具体的支付实现依赖方向被反转了。构造器注入的好处是依赖一旦传入就不可变且对必填依赖有强制约束编译期能直接看出这个类需要什么。回到 Spring 本身Spring 的 IoC 容器由 org.springframework.beans 和 org.springframework.context 两个基础包支撑BeanFactory 是核心接口负责 bean 的定义注册和实例获取ApplicationContext 是它的子接口扩展了 AOP 集成、国际化消息、事件传播等能力。Spring 框架至今已经集成 20 多个模块大致分成核心容器、数据访问/集成、Web、AOP、工具、消息和测试这几类。面试答到这里就可以自然过渡到下一题了。2.2 BeanFactory 与 ApplicationContext选型要看功能边界BeanFactory 可以理解为一个按需实例化的工厂它保存了所有 bean 的定义客户端请求时才会创建实例属于懒加载。很多 Spring 内部的基础设施还是在使用它。而 ApplicationContext 在 BeanFactory 之上做了扩展常见区别可以从三个点来记支持国际化文本消息统一的资源文件读取方式比如 classpath 或文件路径支持事件发布与监听能让注册在容器里的 bean 互相解耦通信。在 web 应用里还有针对 Spring MVC 的 WebApplicationContext它额外封装了 Servlet 上下文相关的配置读取能力。实际编码中最常见的是这三种加载方式。我平时引导别人入门时会给出这样的对照代码// 从 classpath 下的 XML 配置文件加载上下文 ApplicationContext ctx new ClassPathXmlApplicationContext(applicationContext.xml); // 从文件系统路径加载 XML ApplicationContext fileCtx new FileSystemXmlApplicationContext(/home/app/config/applicationContext.xml); // 从 Java 配置类加载Spring 3.0 之后的推荐做法 ApplicationContext annoCtx new AnnotationConfigApplicationContext(AppConfig.class);第一行适合在工程内部使用配置文件随项目打包进 classpath第二行适合本地调试时指向一个绝对路径第三行则是现在主流项目的做法直接用配置类替代 XML。想知道容器里有没有某个 bean可以用 ctx.containsBean(orderService) 判断不要靠猜。面试时常见的一个追问是ApplicationContext 启动时会不会把所有 bean 都创建出来。答案是 ApplicationContext 默认会预实例化所有 singleton 的 bean所以启动变慢通常发生在容器加载阶段而 BeanFactory 是等调用 getBean 时才去实例化。这个细节可以作为加分项说出来。2.3 三种配置方式XML、注解、Java Config 怎么选Spring 3.0 之前XML 是唯一选择3.0 之后 Configuration 和 Bean 把 Java 配置推上了主流注解扫描从 2.5 就开始了。三者不是互斥关系常见工程里往往是 Java Config 定义基础设施注解标注业务组件XML 只在历史项目中继续存在。先说基于 XML 的配置。核心结构是以 为根内部放 定义和属性注入。下面的例子展示了一个 viewResolver 和一个 REST 模板的注册beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd bean idviewResolver classorg.springframework.web.servlet.view.BeanNameViewResolver/ bean idrestTemplate classorg.springframework.web.client.RestTemplate/ /beans这种写法的好处是集中可见所有 bean 都在一个文件里能看到缺点是配置量大、类型安全差id 或 class 写错要运行期才暴露。基于 Java 的配置用 Configuration 标记配置类Bean 标记方法。方法返回值就是要交给容器管理的对象Configuration ComponentScan(basePackages com.example.service) public class AppConfig { Bean public PaymentClient paymentClient() { return new PaymentClient(sandbox-key); } }ComponentScan 让容器去指定包下扫描带 Component、Service、Repository 的类并自动注册为 bean。注意 basePackages 的范围要准确太大容易扫进无关类太小则 bean 缺失。基于注解的配置则需要先打开注解装配开关。老 XML 工程里要在配置文件中加 context:annotation-config/ 告诉容器启用 Required、Autowired 这些注解。Spring Boot 项目里因为自动配置已经处理掉了很多人没意识到这个开关的存在。注解注入会在 XML 注入之前被处理同一个属性如果两种方式都写了XML 的配置会覆盖注解注入的结果这个顺序细节在排错时很有用。如果是在传统 web 工程里用 Java 配置还要把 web.xml 里默认的 XmlWebApplicationContext 换成 AnnotationConfigWebApplicationContextcontext-param param-namecontextClass/param-name param-value org.springframework.web.context.support.AnnotationConfigWebApplicationContext /param-value /context-param context-param param-namecontextConfigLocation/param-name param-valuecom.example.AppConfig/param-value /context-param这段配置的意义在于让 ContextLoaderListener 不再去读 XML而是按配置类初始化容器。如果不配 contextClass容器会按默认的 XmlWebApplicationContext 去加载自然找不到 AppConfig启动阶段就会报错。三种配置方式没有绝对的优劣。我的习惯是团队新项目用 Java Config 注解扫描老项目尽量保持 XML 不动避免混用导致的排查成本。3. Bean生命周期与作用域从创建到销毁的每一环都要能说出来3.1 生命周期回调四种方式一前一后两段Spring bean 的生命周期其实可以拆成“初始化前”和“销毁前”两段。容器负责在实例化后、使用前执行初始化回调在容器关闭前执行销毁回调。Spring 提供了四类方式来实现实现 InitializingBean 和 DisposableBean 接口实现特定的 Aware 接口如 BeanNameAware、ApplicationContextAware在 XML 或配置中指定 init-method 和 destroy-method使用 JSR-250 的 PostConstruct 和 PreDestroy 注解。在 Spring 2.5 之前接口方式是主流现在项目里基本用注解或 init-method。这里有一个执行顺序要记住构造器先执行然后是 PostConstruct再是 afterPropertiesSet()最后是自定义的 init-method。销毁阶段顺序相反PreDestroy 先于 destroy-method。这个顺序经常被当追问点能完整背出来说明对生命周期有真实理解。XML 里自定义初始化方法的写法如下bean iddemoBean classcom.example.task.DemoBean init-methodcustomInit destroy-methodcustomDestroy/对应的 DemoBean 里需要提供两个无参方法比如public class DemoBean { public void customInit() { System.out.println(init-method 执行初始化连接池); } public void customDestroy() { System.out.println(destroy-method 执行释放连接池资源); } }使用 Bean 注解时等价写法是在注解参数里指定Bean(initMethod customInit, destroyMethod customDestroy) public DemoBean demoBean() { return new DemoBean(); }这里的重点不在于方法名而在于理解容器对这些回调的调用时机。文件里的 init-method 不检查方法参数写了带参数的 init 方法会运行期报错。3.2 五种作用域穿好单例之外的三件Web外套Spring 容器里 bean 有五个作用域前两个在任何应用里都生效后三个是 Web 容器环境才有的。为了面试梳理我直接拿表格出来对比作用域实例数量存活范围适用场景singleton每个容器一个容器启动到关闭无状态服务类、DAO、工具类prototype每次请求一个创建到被回收有状态的模型对象、临时任务request每次 HTTP 请求一个请求开始到结束请求上下文数据session每个会话一个会话创建到过期用户信息、购物车global-session所有 portlet 共享一个全局生命周期Portlet 应用中的全局变量singleton 是默认作用域一个容器里只创建一个实例这个行为由 BeanFactory 维护。prototype 相反每次 getBean 或者注入时都新建一个。注意一个容易说错的点prototype 的 bean 创建后容器就不管它了destroy 回调不会执行资源清理需要业务代码自己负责。request 和 session 作用域比较好理解一个对应请求生命周期一个对应会话生命周期。global-session 和 Portlet 容器相关普通 Web 应用几乎用不到但面试里偶尔会被提起能说出“它是所有 portlet 共享的全局存储”就够用。Web 作用域在注解方式下用 Scope 标记比如 Scope(request)。实际业务中我基本只用 singleton 和 prototype。把有状态的 bean 从 singleton 改成 prototype是处理并发问题的快速手段但换作用域之前要先想清楚频繁创建对象的成本是否可接受。3.3 单例bean是否线程安全答案是不一定这是个高频题也是很多人翻车的地方。Spring 框架本身不会对单例 bean 做线程包装所以“Spring 的单例 bean 是线程安全的”这个说法本身就是错的。一个没有可变成员变量的 Service 类多个线程共用同一个实例也没有共享状态自然是安全的但如果 bean 里有一个 HashMap 或者一个计数器字段线程并发修改时就会出现竞争。解决思路按照严重程度从低到高排列确认这个状态是不是必须放在 bean 里优先改成无状态无法改的把作用域换成 prototype必须共享的用 ConcurrentHashMap、AtomicLong 或加锁来控制。也可以使用 ThreadLocal 做线程隔离但要注意请求线程池复用时的清理问题避免数据串扰。举个例子下面这个 bean 如果在 singleton 作用域下被多个线程读到count 的自增就不是安全的Component public class VisitCounter { private int count 0; public int increment() { return count; } }把 int 换成 AtomicInteger或者把作用域改成 prototype都能解决但语义完全不同。面试时能说出“Spring 不保证线程安全开发者要自己负责”这个核心点再补一句“大多数无状态 bean 正好天然安全”基本就能拿分。3.4 Aware 接口与容器回调容易被追问的细节除了生命周期接口Spring 还定义了一组 Aware 接口用来让 bean 获得容器自身的能力。比如 BeanNameAware 可以在 bean 初始化时拿到自己在容器中的 idApplicationContextAware 可以拿到 ApplicationContext 引用。它们的回调发生在属性注入之后、初始化方法之前这个顺序常被面试官拿来追问。举一个实际场景某个工具类需要动态从容器里按类型获取其他 bean就可以实现 ApplicationContextAware 保存上下文引用。一个简单的写法Component public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext context; Override public void setApplicationContext(ApplicationContext applicationContext) { context applicationContext; } public static T T getBean(ClassT clazz) { return context.getBean(clazz); } }把 SpringContextHolder 注册成 bean 之后工具类里就可以直接 SpringContextHolder.getBean(XxxService.class) 拿到容器里的实例。注意静态字段在多线程读取时没问题但不要在这个 holder 里写入业务状态避免生命周期管理和内存泄漏的麻烦。4. 依赖注入细节实战集合注入、自动装配与构造器选择4.1 集合与 Properties 注入XML 里四种标签要分清面试里有一类题专门考 XML 注入的熟练度比如“怎么把一个 List 注入到 bean 里”。Spring 为四种集合类型提供了对应标签 对应 List对应 Set对应 Map 对应 Properties。注意 List 和 Set 都支持重复值但 Set 会去重Map 的键值可以是任意类型props 的键和值都必须是字符串。下面的 XML 配置是一个经典示例我把其中的值改成了通用字符串方便直接复现bean idjavaCollection classcom.example.JavaCollection property namecustomList list valuealpha/value valuebeta/value /list /property property namecustomSet set valuealpha/value valuebeta/value /set /property property namecustomMap map entry key1 valuealpha/ entry key2 valuebeta/ /map /property property namecustomProperties props prop keyadminadminexample.com/prop prop keysupportsupportexample.com/prop /props /property /bean这里 JavaCollection 中需要有对应的四个属性以及它们的 setter。使用 时键值不允许空值这是和最大的区别。注入的 java.util.Properties 在读取时用 getProperty(admin) 就能拿到配置值常用于邮件配置、外部服务地址等场景。除了标签注入也可以用 util: 命名空间直接创建集合 bean再被其他 bean 引用。这种方式适合集合配置被多处复用的情况避免在每个 bean 里重复写一遍 list。4.2 自动装配byName、byType 和注解三兄弟自动装配是 Spring 减少 XML 配置量的核心机制。XML 层面有 autowire 属性可选值包括 byName、byType、constructor 等。byName 的意思是容器查找 id 和属性名相同的 bean 来自动注入byType 则是按类型匹配一个 bean 来注入。下面是一个按名称装配的示例bean idemployeeDAO classcom.example.dao.EmployeeDAOImpl autowirebyName/要求当前 bean 的属性名恰好存在同名 bean。按类型匹配时如果同类型存在多个候选就会抛出歧义异常这时要么配合 Qualifier 指定名字要么通过 primary 属性指定优先 bean。现在主流做法是用注解。Autowired 可以用在字段、setter、构造器上它本质上是先按类型匹配再按名称兜底。Qualifier 和 Required 是常考的搭配Service public class OrderService { private final PaymentClient paymentClient; Autowired public OrderService(Qualifier(wechatPay) PaymentClient paymentClient) { this.paymentClient paymentClient; } }如果容器里有多个 PaymentClient 实现不加 Qualifier 就会启动失败加上 Qualifier(wechatPay) 后精确指定注入 wechatPay 这个名字对应的实例。Required 用于标注 setter 方法容器启动时会校验该属性是否完成注入如果没注入直接抛异常适合用来兜底必填依赖。使用注解配置前老项目需要在 XML 里开启开关context:annotation-config/这是很多人踩坑的地方写完 Autowired 却报 null先查这个开关有没有打开再看 ComponentScan 有没有扫到对应的包。4.3 构造器注入还是 setter 注入看必需性和可变性这道题几乎每次面试都会出现。构造器注入适合必填依赖对象创建时就把依赖固定住无法在后续被替换有利于不可变设计setter 注入适合可选依赖对象可以先创建再按需设置也方便运行期替换实现但存在对象被构造到一半、依赖尚未就位的窗口。还有一个关键差异是循环依赖。构造器注入遇到 A 依赖 B、B 依赖 A 时容器在创建阶段就会相互等待直接启动失败setter 注入则可以在实例化后再注入Spring 能通过三级缓存处理这类场景。如果面试官问“循环依赖怎么解决”先答 setter 注入 三级缓存再补一句最好的做法是重构消除循环依赖。Spring 团队现在的官方推荐是构造器注入为主这不光是因为不可变性也因为它更容易写出不依赖容器的单元测试。可选依赖可以用 Autowired(requiredfalse) 或 setter 注入来兜底。只有一个候选 bean 时构造器注入可以直接用 getBean 取到最终对象代码路径更清晰。4.4 JSR-250 注解与内部 bean容易忽略的两个考点JSR-250 提供了一组轻量注解Spring 也做了支持。Resource 等价于 Autowired Qualifier 的组合它优先按名称注入找不到再按类型PostConstruct 和 PreDestroy 对应生命周期回调用法上等价于 init-method 和 destroy-method。区别在于命名来自 JDK 标准迁移到其他容器时通用性更好。内部 bean 这个考点出现率不低。所谓内部 bean就是在一个 的 或 里嵌套另一个 这个嵌套 bean 没有 id只能被外层 bean 使用。比如下面这个结构bean idcustomerBean classcom.example.common.Customer property nameperson bean classcom.example.common.Person property namename valuesample/ property nameage value18/ /bean /property /bean内部 bean 的定位是“只为这一个 bean 服务”适合那些不想暴露给容器其他部分的小对象。如果这个 Person 还要被其他 bean 复用就应该提升为独立的 bean 定义。另外注意内部 bean 默认是 prototype 语义每次注入都会新建实例。5. 避坑指南五个高频翻车点与排查思路5.1 Autowired 注入为 null但容器明明有 bean现象代码里写了 Autowired 注入 service运行期一路调用到该字段却发现是 null没有抛异常也没有启动失败。原因分三档一是老 XML 工程没加 context:annotation-config/ 注解装配默认关闭二是类上没加 Component 或 Service容器根本没扫描到它三是 ComponentScan 的 basePackages 写错了路径扫不到目标包。解决先看启动日志里有没有对应的 bean 定义再检查类注解最后确认扫描路径。我一般最快的一招是在字段上临时打个断点看容器初始化时有没有走到 setter再顺着反推配置。这也是为什么我建议新手先看组件扫描路径再看开关。5.2 单例 bean 注入 prototype拿到的永远是一份实例现象把购物车 bean 的作用域配成 prototype却在 singleton 的订单服务里注入它请求两次后发现每次拿到的都是同一个实例。原因singleton bean 在容器启动初始化时就把 prototype 实例注入并固定到了成员变量里后续不再重新创建。作用域只管每个 bean 自身的生命周期管不了被谁持有。解决把这个注入点从字段注入改成方法级获取常见做法是用 ObjectFactory 或者 ApplicationContext.getBean() 在每次调用时取一次。Spring 也提供了 Lookup 方法标注了 Lookup 的方法会被容器重写每次调用都返回一个新的实例。这里还有一个容易被忽略的边界prototype bean 的销毁回调不会被执行释放资源要自己写。5.3 init-method 不生效甚至报错“找不到方法”现象XML 里配了 init-methodcustomInit启动时容器提示找不到对应方法或者发现方法根本没执行。原因init-method 指定的方法必须是无参非静态的如果目标类里有重载方法也容易因为签名不匹配而失败。更隐蔽的情况是 bean 定义被重复加载容器里有两个同名 id 的 bean你改的那份配置根本没被读到。解决先确认方法签名确实是无参的再用单元测试打印出 ApplicationContext 里所有 bean 的 id 列表排除重复定义。定位到重复配置后把其中一个 bean 定义的 id 改掉或删除。若用的是 Java 配置还要检查 Bean(initMethod ...) 里的方法名是否和类里实际方法名完全一致大小写不匹配同样会运行期报错。5.4 byType 自动装配遇到多个同类型候选启动直接失败现象用 Autowired 注入一个 Service 接口接口有两个实现类启动时抛 NoUniqueBeanDefinitionException。原因Autowired 默认按类型匹配接口有多个实现时容器不知道注入哪个。Spring 不会自动选第一个因为顺序不保证直接报错反而安全。解决用 Qualifier 指名字或者在其中一个实现类上加 Primary 指定首选。具体用哪个取决于业务语义主实现用 Primary策略分支用 Qualifier 更清晰。还有一种做法是把注入字段改成 List接口容器会把所有实现按顺序注入进来适合策略模式。5.5 注解注入和 XML 注入同时存在XML 悄悄覆盖了注解现象字段上写了 AutowiredXML 里又配了一个 property结果运行期用的是 XML 里的值排查了大半天。原因Spring 容器处理注入时有先后顺序注解注入先执行XML 注入后执行后执行的自然覆盖了前者的结果。很多人以为两种方式冲突会报错实际上不会。解决一个属性只保留一种注入方式。老工程迁移时先全局搜 XML 里 的 name 和注解字段名是否重叠重叠的先清理再迁移。排查这种问题最快的办法是看运行期对象实际引用的地址再回去翻配置比读文档快得多。提示以上五条基本覆盖了 Spring 基础面试里最高频的“答不出来”场景。每一条都值得自己动手复现一遍复现比背诵更能留下记忆。6. 设计模式与Resource加载用面试真题验证基础掌握程度6.1 框架里藏着哪些设计模式Spring 框架被大量运用了设计模式面试问“列举 Spring 中的设计模式”时背出六七个就够用了单例模式bean 默认作用域是 singleton由容器保证唯一实例工厂模式BeanFactory 本身就是工厂负责创建和管理对象代理模式AOP 通过 JDK 动态代理或 CGLIB 生成代理对象实现切面模板方法模式JdbcTemplate、RestTemplate 把流程固定住只留回调方法给调用方观察者模式ApplicationEvent 配合 ApplicationListener容器发布事件后多个监听器响应适配器模式AOP 中的 AdvisorAdapter把不同类型的增强适配到统一的拦截逻辑。答题的时候不用背定义最好各配一句“框架里在哪里用了它”。比如观察者模式可以接事件机制来说Spring 的事件类型包括 ContextRefreshedEvent、ContextClosedEvent 等发布事件用 applicationContext.publishEvent()监听端实现 ApplicationListener 或者加 EventListener 注解即可。这个组合答法比单独背模式定义更有吸引力也顺手把事件类题目一起覆盖了。6.2 FileSystemResource 与 ClassPathResource 怎么选这两个类在资源加载题里经常出现。ClassPathResource 从 classpath 根路径找文件打包进 jar 之后依然能读到适合加载项目内的配置模板FileSystemResource 直接从文件系统路径读适合外部化配置比如把一份邮件模板放在应用目录之外运维可以直接改。区别一句话就能说明白前者依赖类路径后者依赖真实文件路径。加载失败时先想清楚文件到底在哪个路径体系里不要拿 classpath 的写法去读文件系统的路径。准备面试的时候我习惯把这些容易混淆的类名放进一个小清单里每个类配一个使用场景考前快速过一遍。这份 25 问清单本身也是这样用的——先自己把每个问题的主干答案写一遍再对照原答案看漏掉的点是概念理解偏差还是根本没接触过。从那以后我每次准备 Spring 相关的面试都会强制自己先默写一遍 IoC、生命周期、自动装配这几条主线再用清单核对细节最后把踩过的坑记在本子上。这份清单我整理在一个文件里拿到之后建议先通读一遍再挑三四个自己说不清的问题重点练。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站