新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring依赖注入与Bean生命周期:从XML到三级缓存

发布时间:2026/9/14 2:10:28来源:尧图网络
Spring依赖注入与Bean生命周期:从XML到三级缓存
先说个我在项目里经常遇到的现场一个新来的同事代码写到一半跑来问我“我这个Service里想要用另一个类的方法是不是直接new一个就行”我说你往下写写就知道了结果三天后他改需求发现要换一个实现类得把所有new过的地方全部找出来改一遍当场崩溃。这就是典型的耦合问题。而Spring的依赖注入Dependency Injection简称DI解决的就是这一件事别自己new了把你想要的东西告诉容器容器给你送过来。我见过太多人一上手就追着Spring Boot的注解跑却连最基础的XML配置文件都没写过遇到“Bean装配不上”“循环依赖报错”之类的问题只能瞎猜。这篇文章我把依赖注入和配置文件这条线从头捋一遍从最早的XML方式讲到注解、Java Config再把Bean生命周期和三级缓存原理讲透最后附上手写一个极简IoC的代码保证你看完能理解Spring容器到底在背后干了什么。适合正在学Spring的Java后端开发、准备面试的求职者以及用了很久Spring Boot但没系统补过底层的人。1. 依赖注入到底解决了什么问题1.1 耦合的痛点为什么要放弃直接new先看一个最简单的例子。你写一个订单服务里面需要调用用户服务查询用户信息。最直接的做法是public class OrderService { private UserService userService new UserService(); public void createOrder() { User user userService.findUserById(1L); // 业务逻辑... } }看起来没问题代码也能跑。但问题紧接着就来了如果UserService的构造函数改了比如需要传入一个数据库连接池参数所有new UserService()的地方全部编译报错。如果UserService是个接口你有UserServiceImpl和VipUserServiceImpl两种实现想在某个环境下切换怎么办只能改代码。单元测试也很痛苦你想mock一个假的UserService但这个类是直接new出来的没法替换。这些痛点本质上就是高层模块依赖了低层模块的具体实现两个类死死绑定在一起。依赖注入的思路是把“创建依赖”这个动作从调用方身上拿走交给一个第三方容器统一管理。调用方只需要声明“我需要一个UserService”至于这个UserService是哪个实现、怎么创建出来的调用方完全不关心。1.2 控制反转把“主动权”交出去依赖注入背后有个更大的概念叫控制反转Inversion of ControlIoC。这两个词经常混着用但严格来说IoC是一种设计思想DI是它的具体实现方式之一。用生活里的例子理解你想吃一顿饭自己下厨new对象所有食材、调料、火候都要自己控制这是正向控制。你去餐厅点菜依赖注入你只告诉服务员“我要一份宫保鸡丁”声明依赖至于鸡丁是哪个供应商送的、火候怎么掌握那是厨房的事容器内部逻辑与你无关。你的角色从“生产者”变成了“消费者”这就是控制权的反转。Spring容器扮演的角色就是那个大厨房。你在配置文件或注解里声明“我要一份什么菜”容器在启动的时候把所有的菜统一做好、装盘放到一个仓库容器上下文里。谁需要自己来取即可。1.3 三种注入方式构造器、Setter、字段Spring支持三种依赖注入方式直接对比一下注入方式写法优点缺点适用场景构造器注入在构造函数中传入依赖依赖不可变、强制校验、线程安全参数过多时代码冗长Spring官方推荐必选依赖Setter注入通过setXxx方法注入灵活可重新赋值依赖可能为空、不强制可选依赖、动态替换字段注入用Autowired直接标字段写起来最简洁隐藏依赖、不利于测试不推荐但实际项目很常见我个人的经验是核心依赖用构造器注入可选依赖用Setter注入字段注入能不用就不用。字段注入最大的坑是它把依赖关系藏起来了你看着一个类好像没有依赖其实里面全是Autowired的字段。反射注入绕过构造函数也容易让依赖在测试时不好mock。不过现实中很多项目图省事全用字段注入这种代码短期没问题重构和测试的时候就知道难受了。2. XML配置文件入门最原始的注入方式2.1 从零搭建一个最小XML案例Spring最早期的用法就是纯XML配置。虽然现在已经很少从零写XML了但我强烈建议每个学Spring的人都亲手搭一个因为这里面蕴含着容器最核心的模型。先创建一个Maven项目引入spring-context依赖dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.30/version /dependency然后在resources目录下创建applicationContext.xml内容如下?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd bean iduserDao classcom.example.dao.UserDao/ bean iduserService classcom.example.service.UserService property nameuserDao refuserDao/ /bean /beans对应的两个类package com.example.dao; public class UserDao { public User findUserById(Long id) { // 模拟查询数据库 return new User(id, 张三); } }package com.example.service; import com.example.dao.UserDao; public class UserService { private UserDao userDao; // Setter容器会调用这个方法注入 public void setUserDao(UserDao userDao) { this.userDao userDao; } }启动容器的时候只需要一行代码ApplicationContext context new ClassPathXmlApplicationContext(applicationContext.xml); UserService userService context.getBean(userService, UserService.class);容器启动时会做三件事读取XML文件、解析每个bean标签、按照配置创建对象并进行依赖注入。整个过程对调用方完全透明你拿到的UserService已经是一个被装配好的完整对象。2.2 bean标签里那些容易被忽略的属性bean标签除了id和class还有几个属性在日常开发里经常用到我在这边把所有关键属性列一下属性作用示例idBean的唯一标识iduserServiceclassBean的全限定类名classcom.example.service.UserServicescope作用域singleton或prototypescopeprototypelazy-init是否懒加载默认falselazy-inittrueinit-method初始化方法创建后调用init-methodinitdestroy-method销毁方法容器关闭时调用destroy-methodcleanupautowire自动装配模式byName或byTypeautowirebyTypeabstract是否为抽象Bean只做模板abstracttrueparent指定父Bean继承配置parentbaseBean这里要特别提醒一个坑scopeprototype的时候Spring只负责创建和注入不负责销毁。也就是说destroy-method在prototype作用域下不会被执行因为容器压根没有跟踪这些对象的生命周期。我曾经在项目里遇到内存飙升查了半天发现某个bean被配置成了prototype还注册了一个监听器等容器销毁时释放资源结果资源一直没释放。所以prototype对象用完要自己负责清理。2.3 构造器注入与复杂属性装配上面讲的是通过setter注入大多数情况下XML里还会用到构造器注入。假设UserService的构造函数长这样public class UserService { private UserDao userDao; private String serviceName; private int timeout; public UserService(UserDao userDao, String serviceName, int timeout) { this.userDao userDao; this.serviceName serviceName; this.timeout timeout; } }XML配置是这样的bean iduserService classcom.example.service.UserService constructor-arg index0 refuserDao/ constructor-arg index1 value订单服务/ constructor-arg index2 value5000/ /bean这里要注意ref和value的区别ref引用的是容器里另一个bean的idvalue直接赋一个字面量值。如果你要注入的是基本类型或字符串用value如果注入的是另一个对象用ref。再来说集合注入这个写多了容易出错。比如你的类里有一个ListString、一个MapString, Objectbean idconfigBean classcom.example.config.ConfigBean property nameservers list value192.168.1.1/value value192.168.1.2/value /list /property property namecacheConfig map entry keymaxSize value1000/ entry keyttl value3600/ /map /property /bean另外还有一个比较方便的写法叫p命名空间和c命名空间相当于property和constructor-arg的简写。在beans标签上加上xmlns:phttp://www.springframework.org/schema/p和xmlns:chttp://www.springframework.org/schema/c之后就可以这样写bean iduserService classcom.example.service.UserService p:userDao-refuserDao p:serviceName订单服务/ bean iduserService2 classcom.example.service.UserService c:_0-refuserDao c:_1订单服务/p命名空间的-ref后缀表示引用另一个bean不加后缀的就是字面量。c命名空间里_0、_1表示构造函数的第几个参数从0开始数。这两种写法在老的配置里很常见看得懂就行自己写的时候看团队风格。3. 注解驱动注入告别繁琐的XML3.1 为什么XML会逐渐失宠XML配置虽然功能完整但实际用起来有几个明显的痛点配置和代码分离你改一个类名要记得去XML里同步改漏改了启动时直接报错但报错信息有时候根本看不出来是哪行配置引起的。配置文件越来越长一个中型项目几百个beanXML动辄几千行维护成本极高。缺乏编译期检查XML里的classcom.example.UserService写错了只有启动时才会被发现。注解可以把“这个类是Spring管理的Bean”这个信息直接写在类旁边代码重构时跟着走语义也清晰。所以我个人的建议是新项目直接用注解Java Config但XML的理念一定要懂因为你迟早要维护老项目。3.2 核心注解逐个拆解先从最常用的几个说起Component与它的衍生注解Component是通用组件注解任何被标注的类都会被Spring扫描并注册为Bean。而Service、Repository、Controller本质上都是Component的语义化变体分别对应业务层、数据访问层、控制层。功能上它们是等价的使用哪种取决于业务语义。Service public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao userDao; } }Autowired的装配逻辑Autowired默认按类型byType装配。举例来说容器里只有一个UserDao类型的Bean那么标注Autowired的字段或构造器参数直接就能匹配上。但如果容器里有多个UserDao类型比如UserDaoImpl和VipUserDaoImplSpring就不知道你要哪一个了这时候需要配合Qualifier指定bean名字Service public class UserService { private final UserDao userDao; public UserService(Qualifier(vipUserDao) UserDao userDao) { this.userDao userDao; } }这里Qualifier参数写的其实就是bean的名字默认是类名首字母小写也就是vipUserDao。如果你在类上用Repository(customName)改过名字那就写自定义的名字。还有一个比较容易混淆的是Resource。它是JSR-250标准里的注解由JavaEE提供Spring也支持。Resource默认按名称byName装配找不到再按类型。如果你在项目里看到Resource它和Autowired的区别在于Autowired先按类型找Resource先按名字找。两者都能用但从“显式声明依赖名”这个角度看Resource(name xxx)更明确。Value注入配置文件的值Value用来注入配置文件里的值比如Service public class OrderService { Value(${order.timeout:3000}) private int timeout; }这里${order.timeout:3000}的意思是从配置文件读取order.timeout的值如果找不到就用默认值3000。冒号后面就是默认值这个语法在日常配置中非常实用。但要注意Value只支持简单类型如果你想注入一组配置到对象里需要用到后面讲的ConfigurationProperties。3.3 XML与注解的混用策略很多老项目不会一下子从XML迁到注解这个过程中会出现混用的情况。Spring提供了两个关键的桥接方式!-- 开启注解扫描 -- context:component-scan base-packagecom.example/ !-- 引入一个用Configuration标注的Java配置类 -- bean classcom.example.AppConfig/如果你是反过来在注解为主的新代码里想引用一个XML里定义的Bean可以用ImportResourceConfiguration ImportResource(classpath:legacy-context.xml) public class AppConfig { }混用的时候最容易踩的坑是Bean重名。假设XML里定义了一个iduserService的Bean注解扫描时又发现Service(userService)启动会直接报ConflictingBeanDefinitionException提示你找到了同名的Bean定义。解决思路是在迁移时统一命名规范比如XML里的Bean加一个前缀避免和注解Bean冲突。4. Java Config配置类类型安全的替代方案4.1 Configuration Bean组合XML的另一个替代方案是用Java类写配置也就是Java Config。核心玩法是Configuration标注配置类Bean标注方法方法的返回值就是一个Bean。Configuration public class DataSourceConfig { Bean public DataSource dataSource() { HikariDataSource dataSource new HikariDataSource(); dataSource.setJdbcUrl(jdbc:mysql://localhost:3306/demo); dataSource.setUsername(root); dataSource.setPassword(123456); return dataSource; } Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } }看到没有jdbcTemplate方法的参数直接写了DataSourceSpring会自动从容器里找到这个Bean传进来。这就实现了Bean之间的依赖装配而且完全类型安全——参数类型错误在编译期就能发现不用等启动报错。用Bean创建Bean和用Component扫描注册本质上是等价的区别在于Bean更适合那些你无法修改源码的第三方类比如连接池、消息队列客户端等。你不可能去HikariDataSource的源码上加Component只能在配置类里用Bean方法把它包一层。4.2 配置属性外部化从硬编码到配置文件上面那个例子把数据库连接信息硬编码在Java代码里这在真实项目里是不可接受的。正确的做法是把配置抽到application.properties文件里app.datasource.urljdbc:mysql://localhost:3306/demo app.datasource.usernameroot app.datasource.password123456 app.datasource.max-pool-size20然后改造配置类Configuration public class DataSourceConfig { Value(${app.datasource.url}) private String url; Value(${app.datasource.username}) private String username; Value(${app.datasource.password}) private String password; Bean public DataSource dataSource() { HikariDataSource dataSource new HikariDataSource(); dataSource.setJdbcUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } }如果配置项很多挨个用Value写太累更好的方案是ConfigurationProperties绑定成对象Component ConfigurationProperties(prefix app.datasource) public class DataSourceProperties { private String url; private String username; private String password; private int maxPoolSize 10; // 必须要有getter和setter public String getUrl() { return url; } public void setUrl(String url) { this.url url; } // 其他getter/setter... }Spring Boot会自动把app.datasource.urlxxx这样的配置项映射到这个对象的url字段上前缀app.datasource起到了分组和命名空间隔离的作用。字段名和配置文件里的key遵循宽松绑定规则比如max-pool-size和maxPoolSize是等价的写MAX_POOL_SIZE也行。这个方案的好处是配置项集中、类型安全、IDE有自动补全提示适合参数特别多的场景。4.3 多环境配置切换Profile机制真实项目一定有开发、测试、生产等多个环境数据库地址、日志级别、开关配置都不同。Spring用Profile机制解决这个问题。Spring Boot里最简单的方式是建多个配置文件application-dev.properties开发环境application-test.properties测试环境application-prod.properties生产环境主配置文件application.properties里写公共配置然后指定当前激活哪个profilespring.profiles.activedev如果用的是XML可以通过beans标签的profile属性来分组beans profiledev bean iddataSource classcom.zaxxer.hikari.HikariDataSource property namejdbcUrl valuejdbc:mysql://localhost:3306/dev_db/ /bean /beans beans profileprod bean iddataSource classcom.zaxxer.hikari.HikariDataSource property namejdbcUrl valuejdbc:mysql://prod-server:3306/prod_db/ /bean /beans用注解方式则在Bean方法上加Profile(dev)或者直接在配置类上标注这样只有激活对应profile时Bean才会被创建。在用profile的时候我踩过一个比较隐蔽的坑spring.profiles.active在单元测试里往往不生效。后来发现测试环境要单独建application-test.properties然后在测试类上用ActiveProfiles(test)指定。如果既不指定也不加这个注解测试跑的时候可能加载的是开发环境的配置连错数据库还不自知。5. Bean生命周期与三级缓存5.1 Bean从创建到销毁完整流程网上关于Bean生命周期的面试题很多但这个概念不是只有面试才用得上。理解生命周期你才能知道在什么时候做初始化逻辑、什么时候做资源清理遇到“初始化方法没执行”“AOP不生效”之类的问题才能快速定位。完整流程可以分成两大阶段阶段一创建与初始化实例化Spring根据Bean定义创建一个对象相当于执行new。属性填充如果Bean有依赖容器执行依赖注入。初始化前置执行BeanPostProcessor#postProcessBeforeInitialization。执行初始化方法先执行InitializingBean#afterPropertiesSetSpring接口方法再执行配置的init-method自定义方法。初始化后置执行BeanPostProcessor#postProcessAfterInitialization这里也是Spring AOP创建代理对象的关键时机。阶段二使用与销毁此时Bean已经可以正常被使用了。容器关闭时执行DisposableBean#destroy和配置的destroy-method。BeanPostProcessor是Spring一个极其重要的扩展点像Autowired的注入、Async的代理、AOP的切面代理全部是通过各种BeanPostProcessor实现的。你说Spring是个容器不如说它更像一个流水线BeanPostProcessor就是流水线上的各个工位。5.2 三级缓存解决循环依赖的原理循环依赖指的是A依赖B、B依赖A两者互相引用。在Spring中singleton的Bean默认情况是可以正常创建的靠的就是三级缓存。三级缓存对应DefaultSingletonBeanRegistry里的三个Map缓存存储内容用途一级缓存完整的成品Bean最终拿到的Bean二级缓存早期暴露的原始Bean未完成属性填充缓存半成品保证单例三级缓存ObjectFactory工厂生产早期Bean的代理引用生成代理对象并解决提前引用问题整个流程大概是这样的创建A时A发现自己需要B于是先去创建B。B在创建过程中发现需要A这时候A虽然还没完全走完初始化流程但已经实例化出了原始对象并且这个原始对象被放进了三级缓存。B从三级缓存里取出A的早期引用完成自己的创建回到A的创建流程A再完成后续的依赖注入。这里要特别注意三级缓存放的是ObjectFactory而不是直接放对象是一个() - getEarlyBeanReference(beanName, bean)的lambda。为什么要包一层工厂因为Spring需要在这个时机判断这个Bean是否被AOP代理了。如果被代理了B拿到的应该是A的代理对象而不是原始对象。代理对象必须在真实对象创建早期就暴露出来否则B持有的引用一直是原始对象AOP就会失效。这个判断放到工厂里延迟执行能保证在需要的时候才触发也避免了提前创建代理对象造成的问题。5.3 什么情况下循环依赖会直接报错三级缓存能解决循环依赖但有两个前提限制限制一必须是singleton作用域。prototype的Bean不缓存A创建B、B创建A两边都没法提前暴露对象直接死循环报BeanCurrentlyInCreationException。限制二不能是构造器注入的循环依赖。假如A的构造函数需要BB的构造函数需要A那A在实例化阶段就卡住了——对象还没new出来根本没机会放进三级缓存。所以构造器注入碰上循环依赖是必然失败的。我的建议是不要把解决循环依赖的希望寄托在三级缓存上而是从设计上避免循环依赖。循环依赖本身往往意味着你的类职责边界没划分清楚。出现A、B互相依赖时先想想能不能拆出一个C来放公共的依赖或者用事件驱动、延迟加载等方式解耦。Spring的三级缓存更像是兜底方案而不是让你肆无忌惮制造循环依赖的借口。6. 手写一个极简依赖注入容器6.1 容器的最小模型理解了Spring的做法之后我们亲手实现一个简化的IoC容器。先想清楚一个容器最少需要哪几样东西一个注册表存Bean名字和Bean实例的映射关系。实例化能力通过反射创建对象。依赖发现与注入找到Bean里哪些字段需要注入然后从注册表里取依赖塞进去。我先定义一个注解用来标记哪些字段需要被注入package com.example.miniioc; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Retention(RetentionPolicy.RUNTIME) Target(ElementType.FIELD) public interface MyAutowired { }6.2 核心容器实现然后写容器类核心逻辑用反射完成package com.example.miniioc; import java.lang.reflect.Field; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class MiniIocContainer { private final MapString, Object beanMap new ConcurrentHashMap(); /** * 注册一个Bean创建实例、放入容器、注入依赖 */ public void register(Class? clazz) throws Exception { String beanName lowerFirst(clazz.getSimpleName()); Object instance clazz.getDeclaredConstructor().newInstance(); beanMap.put(beanName, instance); injectDependencies(instance); } /** * 遍历字段找到标注了MyAutowired的字段并注入 */ private void injectDependencies(Object instance) throws IllegalAccessException { Field[] fields instance.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(MyAutowired.class)) { String dependencyName lowerFirst(field.getType().getSimpleName()); Object dependency beanMap.get(dependencyName); if (dependency null) { throw new IllegalStateException(找不到依赖: dependencyName); } field.setAccessible(true); field.set(instance, dependency); } } } /** * 按名称获取Bean */ SuppressWarnings(unchecked) public T T getBean(String name) { return (T) beanMap.get(name); } private String lowerFirst(String className) { return Character.toLowerCase(className.charAt(0)) className.substring(1); } }测试一下package com.example.miniioc; public class UserDao { public String getUserName() { return 张三; } }package com.example.miniioc; public class UserService { MyAutowired private UserDao userDao; public void printUserName() { System.out.println(用户名称: userDao.getUserName()); } }package com.example.miniioc; public class Application { public static void main(String[] args) throws Exception { MiniIocContainer container new MiniIocContainer(); container.register(UserDao.class); container.register(UserService.class); UserService userService container.getBean(userService); userService.printUserName(); } }运行结果会输出用户名称: 张三说明UserService里的userDao字段被成功注入了。这个简化版容器和Spring的差距在哪儿首先是生命周期管理——真实Spring有BeanDefinition、BeanPostProcessor、aware接口回调等一整套机制其次是作用域支持——我们只有一个单例Map没有prototype再次是代理与AOP——Spring能通过三级缓存和BeanPostProcessor生成代理对象我们的容器做不到。但核心的思想是一致的反射创建对象 容器统一管理 自动装配依赖。亲手写一次你对“IoC容器”这个抽象概念的体感会完全不一样。6.3 一个小扩展解决循环依赖的问题给这个容器加循环依赖支持其实也很简单。核心思路是仿照Spring的二级缓存先预创建所有Bean的实例再统一注入依赖。改进版如下public void registerAll(ListClass? classes) throws Exception { // 第一遍只创建实例不注入依赖 for (Class? clazz : classes) { String beanName lowerFirst(clazz.getSimpleName()); Object instance clazz.getDeclaredConstructor().newInstance(); beanMap.put(beanName, instance); } // 第二遍统一注入依赖 for (Object bean : beanMap.values()) { injectDependencies(bean); } }这个方式虽然简单粗暴但能直观解释Spring循环依赖的核心解法之一把“创建”和“装配”解耦。对象先new出来放在那里互相引用的时候都能找到对方自然就不会死循环了。真实Spring的三级缓存更精细还要考虑代理和生命周期回调但本质思路是共通的。7. 常见问题与排查技巧实录7.1 高频异常速查表依赖注入相关的报错我日常收到最多的就是下面这些直接整理成一张速查表异常信息原因排查方向NoSuchBeanDefinitionException容器里根本没有这个Bean检查类是否加了Component/被扫描到检查bean名字是否拼错NoUniqueBeanDefinitionException按类型找Bean时找到多个用Qualifier指定bean名字或把某个候选Bean标注PrimaryBeanCurrentlyInCreationException构造器注入发生循环依赖或prototype循环依赖检查是否存在构造器循环引用改成setter注入或者重构BeanDefinitionStoreExceptionXML解析出错或类路径不一致检查XML格式、xsd版本、class路径IllegalArgumentException: Property xxx is required注解配置里遗漏了必填属性检查Value、ConfigurationProperties绑定是否完整7.2 配置文件相关的避坑经验配置文件的坑往往比代码本身的坑更难排查因为“编译不报错但运行就是不对”。分享几个真实踩过的坑一占位符没有被解析字段值等于字符串本身。比如Value(${order.timeout})没生效注入进来的字面量就是${order.timeout}。这种情况通常是忘了配PropertySourcesPlaceholderConfigurer或者配置类里的静态方法挡住了Bean定义。Spring Boot项目里一般不会有这个问题但如果你在纯Spring的XML项目里用Value一定要确认XML里有context:property-placeholder locationclasspath:application.properties/或者Java配置里Bean public static PropertySourcesPlaceholderConfigurer propertyConfigurer() { return new PropertySourcesPlaceholderConfigurer(); }注意在Spring 5.1 搭配 Spring Boot 的环境里这个Bean往往由SpringBootApplication内部的机制自动提供所以很多人不会意识到它的存在。一旦脱离Boot环境这类问题立刻浮出水面。坑二配置文件的编码问题。application.properties里写了中文注释在Windows上如果文件编码是GBKSpring读取时可能乱码严重的时候直接把后面的配置项都解析错了。解决方法是统一把配置文件编码设置为UTF-8或者尽量用Unicode转义关键是要在IDEA里给.properties文件显式设置编码。坑三profile不生效。有时候你在application.properties里写spring.profiles.activedev但启动后发现读取的还是默认配置。先检查名字有没有拼错再检查是否有多处指定profile的入口比如命令行参数--spring.profiles.activeprod的优先级比配置文件高一旦命令行指定了配置文件里的就被覆盖了。碰到这种情况用spring.profiles.active的优先级顺序从高到低是命令行参数、JVM系统属性、环境变量、配置文件。7.3 依赖注入设计层面的建议除了解决报错我特别想强调几个写代码时容易被忽略的原则Bean的可见性意识。Spring的Bean默认是单例的这意味着它天然带有共享状态。如果你的Bean里有个可变成员变量在高并发场景下就是安全隐患。我在项目里见过有人把用户请求信息存到成员变量里结果不同用户的数据互相串了排查了很久才发现是单例Bean共享状态的问题。可变状态应该通过方法参数、ThreadLocal等方式管理尽量不要作为Bean的字段。构造器注入的强制校验。用字段注入时Spring可以注入一个null值而你完全无感知。用构造器注入则不同只要能构造成功依赖就一定存在编译器就帮你做了一层空指针防护。所以对于必选的依赖构造器注入是更安全的选择。不要滥用Autowired注解。如果一个类里有超过四五个Autowired说明这个类的职责太多了很可能违反单一职责原则。此时应该考虑拆分类而不是继续往里堆依赖。最后再分享一点个人经验Spring的依赖注入和配置文件这套东西我前前后后接触了差不多十年。从最早的纯XML项目到注解驱动再到Spring Boot的自动配置看起来是配置方式一直在变但背后的容器模型几乎没有变过——一直是“统一管理Bean 自动注入依赖 生命周期回调 AOP扩展”这一套。很多刚入行的朋友直接学Spring Boot上手确实快可一旦遇到DependsOn、BeanPostProcessor、BeanFactoryPostProcessor这类底层概念马上不知所措原因就是跳过了基础阶段的积累。我的建议很直接找一个小项目先用XML方式搭一遍再换成注解Java Config方式搭一遍期间把手上的异常挨个踩一遍。这个过程可能会慢但能帮你在脑海里建立起一幅完整的Spring地图。等你看懂了容器是怎么把一个个Bean串起来的再回头看Spring Boot的SpringBootApplication和一堆自动配置类就会觉得不过如此。对了最后补一个小技巧给ComponentScan指定包路径时我习惯把路径写具体到业务模块而不是直接用根包扫描。比如ComponentScan(basePackages com.example.order)这样能避免扫描到多余包、减少启动时间也能防止不小心扫描到测试类造成Bean冲突。这个习惯我踩了两次坑之后才养成写下来希望你不用踩。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

ARM交叉编译本质:ABI、架构与工具链的深度协同 2026/9/14 3:10:32

ARM交叉编译本质:ABI、架构与工具链的深度协同

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
VOC数据转YOLO训练:类别映射、坐标归一化与数据体检实战指南 2026/9/14 3:10:32

VOC数据转YOLO训练:类别映射、坐标归一化与数据体检实战指南

简介:面向交通道路目标检测任务的多类别标注数据集,覆盖车辆、行人、自行车与摩托车等常见交通参与者,适合计算机视觉初学者入门实践,也适合自动驾驶、智慧交通等方向的开发者在真实道路场景下进行模型训练与算法验证。压缩包约12…

阅读更多 →
ARM交叉编译本质:ABI对齐与架构直觉 2026/9/14 3:10:32

ARM交叉编译本质:ABI对齐与架构直觉

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
AI英语学习APP开发:核心技术架构与实战经验 2026/9/14 3:10:32

AI英语学习APP开发:核心技术架构与实战经验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
基于STM32计算器实战:LCD1602与矩阵键盘驱动全解析 2026/9/14 3:10:32

基于STM32计算器实战:LCD1602与矩阵键盘驱动全解析

做单片机课设的时候,老师给的题目单里十有八九有一项“基于STM32的计算器”,甚至很多初学嵌入式的朋友第一个上手项目也是它。这题目看起来没什么技术含量,但真动手做起来,从硬件到软件全是细节——LCD1602这个经典老屏幕的驱动时…

阅读更多 →
uniapp校园二手书城:Vue2跨端实战项目 2026/9/14 3:07:32

uniapp校园二手书城:Vue2跨端实战项目

简介:这是一套面向高校计算机及相关专业学生的毕业设计级项目源码,基于uniapp与Vue2开发的校园二手书城微信小程序及APP双端应用,解决校园教材循环利用与本地化交易场景需求,适用于毕业设计、课程设计、团队实训及前端进阶学习。压…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞