☰
Spring IOC开发步骤详解:从环境准备到Bean装配实战
2026/10/21 9:34:04 网站建设 项目流程

很多人在学 Spring 的时候,都会经历一个特别常见的卡壳:IOC 概念背得滚瓜烂熟,依赖注入的例子也能照着画出来,可一关掉教程,自己打开 IDE,完全不知道第一步该干嘛。早几年我学的时候也一样,看了不少理论,结果动手做的时候还是到处问人。后来我把 Spring 的开发步骤拆开梳理了一遍,发现整个流程其实非常固定,本质上就是五件事:环境准备、工程骨架、Bean 装配、业务编码、启动验证。

这篇博文就按这个顺序写。我把这套步骤实践在一个模拟订单管理项目上,项目不复杂,但能真跑、能验证、能看到 Bean 被容器管理起来,新手照着走一遍,基本能摆脱“会背概念不会开发”的状态。如果你已经写过一些 Spring Boot 代码,但始终不清楚底层到底替你干了什么,这篇文章也能帮你把断掉的知识链补上。

1. 开发步骤的全貌:Spring 项目是“容器接管”而不是“代码堆砌”

1.1 先弄清终点:我们要交付的是一个由容器统一管理的 Java 应用

学 Spring 之前,大多数人写 Java 是这样的:自己 new 一个对象,自己把依赖传给构造器,程序结束时对象自己回收。听起来没什么问题,但只要项目一复杂,麻烦就来了。比如一个业务类依赖数据库访问类,数据库访问类又依赖数据源配置,你每在一个地方创建业务类,就得把整条依赖链重新组装一遍。改一个实现类,所有 new 过它的地方都得跟着动。

Spring 做的事情,通俗地说,就是把“创建对象”和“维护对象关系”这两件事从你手里拿走了。你写的类依然是你写的类,但对象实例化、依赖组装、生命周期管理,全部交给一个叫容器的东西处理。所以 Spring 开发步骤的本质,不是“写一堆类再把它们手动连起来”,而是“告诉容器有哪些类需要被管理,以及它们之间是什么关系”。

这个思维转换很关键。很多人写 Spring 代码不顺手,就是因为还在用“Java 对象怎么创建”的旧思路,遇到 Bean 就下意识想 new,遇到依赖就想手动赋值。其实在 Spring 的世界里,你只需要做三件事:声明一个类是 Bean、告诉容器去哪里扫描、在需要的地方声明“我要什么”。容器会像一台自动组装机一样,在启动阶段把整个对象图建好。

1.2 五步全景图:每一步的产物是什么

我在练习的时候,给自己画了一张简表,几乎每次卡住都会回到这张表里定位问题。表不长,但对新手理清步骤非常有用。

步骤要做什么核心产物最常见卡点
1 环境准备安装 JDK、构建工具,确认 IDE 可用一个能编译的最小 Java 工程版本不匹配,依赖下载失败
2 搭建骨架创建工程目录、引入 Spring 依赖构建脚本和配置文件配置文件放错位置
3 Bean 装配定义类、声明 Bean、配置扫描路径被容器管理的对象集合扫描不到类、循环依赖
4 业务编码写分层业务,加事务和 AOP 配置可用的业务逻辑配置不生效、注入报错
5 启动验证写启动入口、运行、看日志输出一个真正运行的 Spring 应用Bean 找不到,容器没启动

这张表看起来简单,但它其实对应了整篇博文的安排。后面几个章节就是这张表的展开版。在练习阶段,每完成一步,我都会停下来确认“这一步的产物真的存在吗”,而不是急着往下冲。比如环境装好了没有,我会命令行敲一下版本命令验证;配置文件放对没有,我会看一眼 classpath 路径;容器启动没启动,我会看控制台有没有打印出初始化日志。这种确认习惯,比记住任何 API 都重要。

1.3 为什么顺序不能乱:三个最典型的“跳步事故”

我见过不少新手(包括当年的我)犯一个共同毛病:觉得环境、骨架这些准备工作太基础,直接跳到写业务代码。结果一运行,问题全来了。这里说三个我亲眼见过的高频事故,帮你理解为什么步骤顺序不能乱。

第一个是跳过配置直接写类。有同学写好了一个 Service 类和 DAO 类,用 IDEA 一运行,报NoSuchBeanDefinitionException。原因很简单:类上没加任何 Spring 注解,配置文件里也没声明 Bean,容器根本不知道这些类的存在。在 Spring 的世界里,类是要“登记”给容器管的,跳过登记步骤,后面全白搭。

第二个是环境不确认就开干。有同学 JDK 装的是新版本,IDE 里默认编译级别又是旧的,结果构建工具下载依赖时各种报错,卡在环境上半天。环境这种东西,越往后排查越难,因为报错信息往往让你看不懂根源。

第三个是没写启动入口就想着跑 Web 功能。Spring 应用不管多复杂,启动都得有一个“创建容器、加载配置”的动作。跳过这个动作,直接点 main 方法,结果容器压根没起来,当然什么都跑不出来。所以后面我在第五章会专门讲启动入口怎么写,以及怎么确认容器真的起来了。

2. 环境与骨架:30 分钟内搭出一个能跑的空工程

2.1 版本选型:JDK 和构建工具怎么定

这一步很多人觉得没什么好讲的,其实坑全在细节里。我的建议是:JDK 优先选长期支持版本,不要盲目追新。练习时我一般用 JDK 17 这个长期支持版本,稳定、生态也跟得上;如果你所在的环境用旧版本,那也问题不大,关键是 IDE 和构建工具也要同步匹配这个版本,否则编译的时候会出现“错误: 无效的源发行版”这类提示。

装好以后,一定要做一件事:打开命令行,输入java -version和javac -version,确认两个命令都能正常输出。如果 java 能用而 javac 不能用,说明你只装了运行环境没装完整开发包,后续编译会比较麻烦。

构建工具这块,我强烈建议不要手动下载 jar 包往项目里塞。Spring 框架本身有很多模块,模块之间又有依赖关系,手动管理只会让你崩溃。用主流的自动化构建工具(例如 Maven)管理依赖是最省心的选择,核心好处是依赖传递和版本统一:你只需要在构建脚本里声明要用的 Spring 模块,它会把依赖的依赖也一起处理好。用命令构建工具版本命令验证安装无误之后,新建工程这一步就顺畅了。

2.2 工程骨架:目录结构与配置文件的位置

新建工程以后,默认目录结构大概是这样的:src/main/java放 Java 源码,src/main/resources放配置文件,构建脚本放在工程根目录。这个结构不是随便定的,它对应了“编译主代码、复制资源、运行测试、打包部署”这几个构建阶段的分工。如果你手动创建工程,千万别把目录搞乱,尤其是不要把配置文件放进src/main/java目录下。我就这么干过一次,结果资源文件被当成源码处理,打包之后配置文件根本没进 classpath,Spring 一直找不到配置文件,日志还特别不明显。

在src/main/resources下,我一般放一个空的应用配置文件,比如名字叫application.properties,先不写任何内容。为什么要先放一个?因为 Spring 容器启动时要读取配置文件,文件不存在会影响后续验证步骤的判断——到底是配置内容错了,还是文件根本没加载,这个区分清楚能省非常多时间。这一步的目的是先让工程有一个可以被构建工具识别的样子,不追求功能,只追求“能编译、能跑”。

2.3 最小可运行工程:能启动的空容器

如果你用的是原生 Spring Framework,最小可运行的工程其实很简单:一个普通 Java 类做入口,里面初始化 Spring 容器,然后随便读一个 Bean 出来。这一步很多人会跳过,因为他们觉得“能跑出什么?”但我的经验是,先让一个空容器成功启动,再往里面加东西,后面排查问题会非常轻松。

我当时的做法是新建了一个AppDemo类,里面写了三行代码:准备一个配置文件路径,拿这个路径创建一个 Spring 容器,打印一句启动成功。那时我的配置文件里只声明了一个最普通的 Bean:一个没有任何业务逻辑的占位类DemoInitService。跑起来之后,控制台能看到 Spring 容器初始化的日志,这就说明环境、骨架、配置这三件事全通了。后面所有业务开发,都只是在这个最小模型上不断加类、加配置而已。

3. 把类交给容器:Bean 的定义与装配是 Spring 开发的核心步骤

3.1 为什么“装配”比“编程”更重要

我觉得 Spring 开发里最核心的一步,不是写算法也不是写 SQL,而是装配——也就是把类和类之间的关系通过配置或注解描述给容器。这一步如果做不好,后面业务逻辑写得再漂亮都跑不起来。

举个例子。没有 Spring 的时候,你写一个订单服务,里面直接创建了一个库存服务对象,两个类死死绑在一起。之后想换一个库存服务的实现,就得改订单服务的代码。有 Spring 之后,订单服务只需要声明“我需要一个库存服务类型的对象”,至于这个对象是谁、怎么创建、是不是单例,全由容器搞定。到时候想换实现,改配置或换注解就行,业务类基本不用动。

这就是为什么我觉得“装配”是核心。新手写 Spring 项目,第一步不是写复杂的业务方法,而是学会怎么把一个普通 Java 对象变成容器里的 Bean,怎么让一个类把另一个类“接住”。这件事通了,后面看 Spring Boot 的代码才会觉得顺理成章。

3.2 三种装配方式:XML、注解扫描、Java 配置类

Spring 提供了三种比较主流的 Bean 装配方式,新手至少要清楚它们的存在和适用场景。虽然现在新项目很少用 XML 了,但读老代码的时候经常会碰到,完全不认识会吃亏。

装配方式怎么声明 Bean怎么启动容器适合场景
XML 配置<bean>标签ClassPathXmlApplicationContext遗留项目、配置与代码分离需求
注解扫描@Component、@Service等配置文件里开启扫描或@ComponentScan大多数新项目
Java 配置类@Configuration+@BeanAnnotationConfigApplicationContext无 XML、类型安全、复杂装配场景

我的建议是:新项目优先用“注解扫描 + Java 配置类”,XML 能看懂就行,不用深入。注解扫描的好处是简单,类上一标@Service,容器启动时就能自动发现;Java 配置类的好处是装配逻辑看得见,尤其是项目里有一些不能直接加注解的第三方对象(比如数据源)时,用@Bean手动声明最合适。

这里有一个新手特别容易忽略的点:@Component和它的衍生注解@Service、@Repository、@Controller从功能上讲并没有本质区别,都可以让容器扫描到,只是语义上划分了“业务层、数据层、控制层”。所以如果你在某层用了@Service,扫描同样能生效;如果你把注解加错层了,也暂时不会报错,但从可维护性角度讲,还是各层用各层的注解比较好。

3.3 依赖注入:构造器、Setter、字段注入怎么选

装配好 Bean 之后,下一个问题就是:一个 Bean 怎么拿到它依赖的另一个 Bean?Spring 支持三种常见的注入方式。

字段注入最省事,直接在属性上标@Autowired就行。但它有个隐患:依赖关系完全隐藏,单测的时候不方便手动组装对象,而且如果两个 Bean 互相依赖,字段注入很难在启动阶段暴露问题。构造器注入是我个人默认的选择:依赖在创建对象时必须传进来,对象一旦建好就不变了,测试时也能很自然地往里传假对象。Setter 注入更适合那种“可有可无”的依赖,比如某些可选插件。

配置上有个细节:在一个类只有单个构造器的情况下,Spring 会自动使用这个构造器完成注入,不需要额外加注解;如果类里有多个构造器,最好明确指定该用哪个。我见过一些同学在这里遇到NoUniqueBeanDefinitionException,就是因为容器里有两个同类型 Bean,不知道该注入哪个。解决办法通常是给其中一个 Bean 指定名称,或者在注入处用@Qualifier指定名字。

3.4 一个完整的 Bean 装配代码示例

说了这么多,不如看一个能跑的最小示例。这段代码模拟的是“订单服务依赖库存服务”的场景,我刻意把类名加了Demo前缀,方便你直接复制到自己的工程里验证。

public interface DemoInventoryService { void deduct(String skuId, int count); } @Component public class DemoInventoryServiceImpl implements DemoInventoryService { @Override public void deduct(String skuId, int count) { System.out.println("扣减库存: sku=" + skuId + ", count=" + count); } } @Service public class DemoOrderService { private final DemoInventoryService inventoryService; public DemoOrderService(DemoInventoryService inventoryService) { this.inventoryService = inventoryService; } public void createOrder() { System.out.println("创建订单"); inventoryService.deduct("sku-001", 1); } }

这个DemoOrderService在创建时需要一个DemoInventoryService,Spring 看到构造器参数后,会自动从容器里找到一个匹配的 Bean 传进来。注意这里我用的是接口类型而不是实现类,这样以后再换别的库存实现,订单服务完全不用改。

启动入口我单独写成一个配置类加一个主类:

@Configuration @ComponentScan(basePackages = "com.demo.spring") public class AppConfig { } public class AppDemo { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); DemoOrderService orderService = context.getBean(DemoOrderService.class); orderService.createOrder(); context.close(); } }

运行这个主类,控制台会依次输出“创建订单”“扣减库存: sku=..., count=...”这些内容,同时 Spring 容器启动日志也会出现。这一步走通,说明你的类和类之间的依赖关系已经被容器成功接管了。

4. 让应用真正有业务价值:分层、事务和 AOP 的落地

4.1 业务分层:从 Demo 到可维护项目的跳跃

只会在 main 方法里调 Bean 不算开发,真正的业务项目一定要分层。最常见的分层是三层结构:Controller 负责接收外部请求,Service 负责业务编排,Dao/Repository 负责数据访问。每一层各司其职,层与层之间通过接口和注入连接起来。

新手最常见的问题,是把业务逻辑全堆在 Controller 里。看起来好像代码也能跑,但一旦业务规则变复杂,Controller 就会膨胀成几千行,改一个逻辑要在一个大文件里翻半天。我的习惯是:Controller 只做参数接收和响应封装,任何规则判断、计算、组合都下沉到 Service。这样测试也好写,Controller 不用启动容器就能测 Service 逻辑。Spring 也给了对应的注解来标识每一层:@Controller、@Service、@Repository。它们的作用不只是语义标注,还影响后续的组件扫描和 AOP 切入。

4.2 事务与 AOP:两个“偷懒”但必须理解的技术点

业务系统里但凡涉及到写数据,事务就躲不开。Spring 的事务管理用起来非常简单,在方法上标@Transactional就行。这里必须理解一个底层事实:这个注解真正起作用的方式,不是方法内部自己控制,而是 Spring 在启动时为这个 Bean 生成一个代理对象,由代理对象在方法调用前开启事务、方法正常结束提交事务、抛出异常回滚事务。

我刚学的时候,以为事务是方法内部自己处理的,后来才意识到是代理机制在起作用。这也是一个高频坑的来源:在同一个类里,一个方法内部直接调用另一个带@Transactional的方法,事务经常不生效。原因就是同类调用走的是this本对象,没有经过代理。解决办法要么把两个方法拆到不同 Bean,要么自己注入代理对象,但最推荐的做法还是拆类,让事务边界放在跨 Bean 的调用上。

AOP 和事务本质是同一个机制。AOP 可以给一批方法统一加日志、权限校验、耗时统计这类横切逻辑,不需要在每个方法里重复写。理解 AOP 的关键是理解“代理”二字:Spring 在容器创建 Bean 时,不是直接给你原生对象,而是可能给你一个代理对象,外部调用经过代理,代理在方法执行前后插入额外逻辑。这就是为什么我一直在强调,Spring 的开发步骤里,Bean 装配是所有机制的地基。地基没搭好,代理都没机会生成。

4.3 外部化配置:切换环境不再改代码

做真实项目时,开发和运行环境的配置往往不一样。数据库地址、缓存地址、各种开关,总不能每次部署前都改代码。Spring 的解决思路是外部化配置:把容易变化的内容放到配置文件里,代码里用占位符读取。

在配置类里,可以用@Value("${config.key}")读取配置项,很多第三方组件的配置也支持类似的占位符方式。我第一次用的时候觉得特别神奇,感觉像把“变量”从代码里搬到了外部。后来我才明白,这一步的价值在于环境隔离:开发环境一套配置,测试环境一套配置,发布环境一套配置,启动的时候指定用哪套就行。新手可能觉得这个功能离自己很远,但从一开始写代码就养成“配置不进代码”的习惯,后面会少踩很多坑。

4.4 Bean 找不到、配不上的排查思路

这一步是我觉得新手到进阶必须掌握的“解题思路”。Spring 启动报错很多,但核心其实集中在几类。第一类是NoSuchBeanDefinitionException,意思是容器里压根没有这个类型的 Bean。我遇到这种情况,会先检查三件事:组件类上有没有加注解;组件类所在的包有没有被扫描到;依赖类型是接口时,容器里有没有对应的实现类 Bean。第二类是启动时直接报循环依赖相关错误,比如BeanCurrentlyInCreationException,说明两个 Bean 互相注入,形成了闭环。这种问题不是靠调注解顺序就能解决的,得从设计上拆开:把一个方向的依赖去掉,或者用延迟初始化临时缓解,但长期看还是要重新划分职责。

这套排查思路能帮你省下大量时间。我见过很多同学一报错就慌,把上下文信息全部打出来到处发。其实 Spring 的报错已经写得很清楚了,关键在于你有没有耐心看异常栈的第一行。先看清楚是“找不到 Bean”还是“循环依赖”还是“配置解析失败”,再决定往哪边查。

5. 启动、验证与排错:代码写完不等于开发结束

5.1 启动入口的两种写法

Spring 应用的启动入口,本质是同一个动作:创建容器、加载配置、准备好所有 Bean。只是在不同的使用场景下,入口的形式不一样。原生非 Web 项目,就是我在第三章写的AnnotationConfigApplicationContext,加载一个配置类就能启动;传统 Web 项目则会把 Spring 容器和 Web 容器整合,由外部容器(比如一个独立的 Servlet 容器)在启动时加载 Spring 配置;而 Spring Boot 提供的自动启动,其实就是把“创建容器、加载配置、启动内嵌容器”这几步打包到一个 main 方法里了。

我建议新手先把非 Web 项目的启动方式亲手写一遍,因为这一步最简单,也最容易观察容器的生命周期。写过一次你就会发现,所谓“启动”,无非是让配置被解析、让 Bean 被创建、让依赖被注入这三件事依次发生。理解了这一点,后面看 Spring Boot 自动配置再神奇,也万变不离其宗。

5.2 怎么确认容器真的管起来了

代码能跑和容器真的管起了所有 Bean,是两个层面的事情。我在练习时有个习惯:启动完成后,把容器里所有 Bean 的名字打印出来。Spring 提供了一个很简单的方法:context.getBeanDefinitionNames(),返回一个字符串数组,里面是所有被容器管理的 Bean 名称。

打开日志看一眼,你会发现除了自己定义的 Bean,容器还会自动注册很多基础设施 Bean,比如处理配置的、处理注解的。这时候你只要确认自己的业务 Bean 出现在列表里,就说明装配成功了。另外一个验证方法,是连续两次getBean获取同一个 Bean,然后用==比较一下。默认情况下 Spring 的 Bean 是单例,两次拿到的应该是同一个对象。这个实验能帮你直观感受到“容器管理生命周期”到底是什么含义。

5.3 启动失败的通用排查顺序

我把启动失败的排查顺序整理成一个固定流程,每次遇到问题都按这个顺序问自己一遍,十次里能解决九次:

排查项该怎么做常见结果
编译是否通过先 clean 再重新编译编译错误会掩盖后续所有问题
依赖是否齐全查看依赖树,确认 Spring 模块存在缺模块会报类找不到
配置文件位置确认配置在 classpath 下位置错则配置根本没加载
扫描包路径确认配置类的扫描范围包含业务包扫描不到则 Bean 缺失
Bean 定义是否重复查看日志里的 Bean 定义冲突重复定义会启动报错
端口或容器冲突Web 场景检查端口占用端口占用表现为启动超时
日志级别临时调成 debug 观察详情报错信息不足导致误判

这个顺序不是随便排的,它的逻辑是从“最底层的问题”逐步往上排查。先确认代码能编译,再确认环境依赖没问题,然后确认配置被加载,最后才排查业务代码里的问题。很多同学一上来就怀疑自己代码写错了,结果编译都没通过,查了半天查了个寂寞。

5.4 我在练习中踩过的三个步骤坑

说几个我实际踩过的坑,每个都对应了一条步骤上的疏忽。

第一个坑是把配置类放在子包里,主类在父包里,扫描路径写的是“根据配置类所在包进行扫描”,结果业务类的包不在它的覆盖范围内,所有 Bean 都没被扫描到。解决办法非常朴素:要么把业务包放到配置类所在包下面,要么显式指定basePackages。从那以后我再也不靠“默认从当前包扫描”的直觉了,永远写明确。

第二个坑是构造器注入的循环依赖。两个服务互相需要对方,理论上功能也说得通,但 Spring 启动时直接报循环依赖错误。这件事让我意识到,循环依赖不是注解能解决的问题,而是设计上的坏味道。正确的做法是拆分职责,让依赖方向变得单向。

第三个坑更隐蔽:我改了配置文件,但 IDE 没有重新编译资源文件,运行的时候还在用旧的 class 输出。当时日志怎么看都正常,代码逻辑也改对了,就是结果不对。后来才发现是构建缓存作怪。现在我的习惯是,改完配置先执行一次干净的重新构建,再运行,绝不在旧产物上浪费时间。

这些坑每个都不复杂,但它们有个共同点:都和“确认每一步真的生效了”有关。Spring 开发步骤本身不难,难的是每一步你都以为自己做了,实际上细节根本没到位。

最后分享一个我练手时常做的小动作:每写完一次 Bean 装配,我不是急着看业务结果,而是先启动容器,用context.getBeanDefinitionNames()看看容器里到底有哪些对象。这个习惯帮我省了非常多的排查时间。你先按这套原生 Spring 的步骤把一个最简单的应用跑通,再去用 Spring Boot,你会明显感觉到 Boot 帮你去掉的工作量,恰好都集中在我前面说的这几步上。步骤本身并不玄,难的是每一步都确认它真的生效了。

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

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

立即咨询