ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java EE开发绕不开的Spring:从IoC容器到Spring Boot实战

Java EE开发绕不开的Spring:从IoC容器到Spring Boot实战 1. 为什么说Spring是Java EE开发路上绕不开的第一课先说结论你可以不写Spring但你几乎不可能在任何一个正规的Java EE企业级项目里绕开它。刚入门那会儿我也想过明明用JDBC加Servlet就能跑起来的系统为什么要引入一套这么重的东西后来真到团队里做需求才发现问题根本不是“能不能实现”而是“实现之后能不能维护、能不能扩展、能不能让十个人同时改代码而不互相踩脚”。Spring这个名字听起来像是个框架但它现在更像Java EE生态里的事实标准。早期Java EE的规范由容器实现EJB那套体系又重又繁琐配置一个业务对象都要写一堆接口和部署描述符。Spring做的事情是把“对象创建、对象关系维护、事务控制、权限拦截”这些通用逻辑从业务代码里抽出来让开发者专注写业务本身。这也是为什么那么多Java EE企业级应用都选择Spring作为底层骨架它不绑架你的架构又可以按需接入持久层框架、消息队列、缓存中间件。这篇内容我不会去复述官方文档而是用一个真实项目从零起步的视角带你把Spring的基本运行逻辑过一遍。适合刚学完Java语法和JDBC、正打算接触第一个企业级框架的读者也适合那些已经会用Spring Boot但还没搞懂底层原理的同学。我会先从思维方式讲起再给完整的环境搭建、示例项目和心得笔记最后附上我踩过的一些坑。2. 从驱动到思想理解Spring之前要先建立容器思维2.1 Java EE原生开发到底卡在哪里我们先聊痛点。传统Java EE开发里一个业务往往拆成这样几层Servlet接收请求Service处理业务逻辑DAO操作数据库。这个分层本身没问题问题出在每一层之间的依赖关系上。举个例子你的UserService需要调用UserDao去查数据库。最常见写法是public class UserService { private UserDao userDao new UserDao(); }这在项目只有一两个类的时候完全够用但一旦系统膨胀起来麻烦就来了。UserDao可能要换成MyBatis实现可能要在真实数据库和测试数据库之间切换可能带缓存逻辑你需要一处一处改new操作。更重要的是这种写法让UserService和UserDao实现类“焊死”在一起单元测试时想用一个假的UserDao替代真数据库变得非常麻烦。Java EE本身也意识到了这个问题提出了依赖查找、依赖注入的规范但实现起来配置冗长。Spring做的就是把这个解决问题的方式做成了体系。2.2 IoC和DI到底是什么意思IoCInversion of Control控制反转是Spring的哲学基础。它的核心意思是对象不再自己创建自己依赖的对象而是把创建和查找的权力交给容器。用一个生活化的类比来解释。你去餐厅吃饭不需要自己种菜、养鸡、搭灶台只需要告诉服务员你要什么后厨会整合所有资源把菜端上来。传统开发模式是你自己下厨房要什么都要自己动手Spring的IoC模式是你当食客餐厅容器负责管理食材Bean和烹饪流程依赖装配。DIDependency Injection依赖注入是IoC的具体实现方式。容器在创建UserService实例时发现它需要一个UserDao就会自动把一个准备好的UserDao对象赋值给它。注入方式主要有三种构造器注入、Setter注入、字段注入。我在实际项目里更推荐构造器注入因为可以保证对象在创建时依赖就是完整的不容易出现空指针问题。Service public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao userDao; } }2.3 Bean、容器与ApplicationContext在Spring的世界里被容器管理的对象统称为Bean。Bean的定义、依赖关系、生命周期都通过配置或注解声明给容器。容器本身最重要的接口是ApplicationContext它就是那个“餐厅”的运营中心。Bean的生命周期在入门阶段不用看太细但有几个关键节点要记住实例化、属性赋值、初始化、使用、销毁。Spring还允许你通过InitializingBean接口或PostConstruct注解在初始化阶段做自定义动作。我们做灰度发布和基础数据校验时就经常在初始化阶段把配置中心的参数预加载到缓存里这样对象一创建就能用不需要业务代码关心这些准备工作。很多新手问我什么时候需要手动new一个对象其实绝大多数时候都不需要。当你发现一个对象要被多个地方共享、需要通过接口切换实现、需要在创建前后执行固定逻辑时都应该考虑把它的管理权交给容器。反过来纯工具类、局部临时变量自己new没问题。3. 环境准备与第一个Spring应用3.1 工具选型与版本选择开发Spring应用最低配置非常简单JDK 17或21、Maven或Gradle、IDEA社区版就足够起步了。我见过不少同学纠结要不要装专业版IDEA实话实说社区版配合Spring Boot插件和一些Maven辅助插件入门完全够用。如果不能上网下载插件用记事本加命令行mvn compile同样能跑通只是效率低一些。版本方面建议优先用Spring Framework 5.3.x以上的版本它支持JDK 8到17。如果是新项目我更推荐直接上Spring Boot 3.x因为它底层已经是Spring Framework 6默认支持Jakarta EE规范以后迁移和升级都轻松。唯一要注意的是Spring 6开始javax包全部替换成了jakarta包互联网上很多老代码直接粘贴过来会编译报错这不是你写错了是命名空间变了。3.2 Maven项目骨架怎么建我习惯用Maven管理依赖重点看pom.xml。入门项目只需要引入两个依赖spring-core、spring-context。Maven会自动把spring-beans、spring-aop、spring-expression等核心模块拉下来。properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.41/version /dependency /dependencies这里说一个很多教程不会提的点不要手动在lib目录里拖jar包。即使你现在不熟悉Maven依赖传递规则也要养成写pom的习惯因为项目一复杂依赖版本冲突靠的就是Maven的依赖树分析。Spring项目里最常见的ClassNotFoundException、NoSuchMethodError基本都是版本不对或依赖缺失引起的。3.3 用XML配置还是注解现在纯注解方式已经是绝对主流但我依然建议新手先看一眼XML配置的方式因为理解了XML写法再看注解就会明白每个注解背后到底做什么。一个最简XML配置如下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 iduserDao classcom.demo.dao.UserDao/ bean iduserService classcom.demo.service.UserService constructor-arg refuserDao/ /bean /beans这段配置的意思非常直白容器里有两个BeanuserDao先创建userService创建时把userDao传进构造器。换成注解方式就是在类上标Component或者Service再在构造器上让Spring自动按类型查找注入。比如刚才的UserService构造器里的UserDao参数不需要任何额外注解Spring在容器里找到唯一匹配的UserDao类型自动注入。3.4 第一个能跑起来的代码代码只有三步。定义一个普通类交给Spring管理从容器里取出来使用。Component public class UserService { public String getUserName() { return demo-user; } }public class Application { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(com.demo); UserService userService context.getBean(UserService.class); System.out.println(userService.getUserName()); context.close(); } }注意AnnotationConfigApplicationContext传包名Spring会扫描这个包下的所有组件。如果你经常发现getBean找不到Bean第一件事检查包名范围是否正确再看类上有没有加合适的注解。这事我Debug过不下十次次次都是低级原因。4. 手写一个极简IoC小容器彻底搞懂Spring运作基础4.1 为什么建议你“手写”一遍Spring很多同学学Spring感觉像看魔术注解一加对象自动就有了。魔术看多了心里会发虚一旦运行时Bean装配报错根本不知道去哪里排查。我强烈建议你做一个练习用纯Java反射实现一个300行以内的微型容器。这个容器只需要做三件事扫描包下指定注解的类、实例化这些类、按字段类型注入依赖。核心代码逻辑并不复杂public class MiniApplicationContext { private MapClass?, Object beanMap new HashMap(); public MiniApplicationContext(String packageName) throws Exception { ListClass? classes scanPackage(packageName); for (Class? clazz : classes) { if (clazz.isAnnotationPresent(MyComponent.class)) { Object instance clazz.getDeclaredConstructor().newInstance(); beanMap.put(clazz, instance); } } for (Object bean : beanMap.values()) { injectDependencies(bean); } } private void injectDependencies(Object bean) throws IllegalAccessException { for (Field field : bean.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(MyAutowired.class)) { field.setAccessible(true); Object dependency beanMap.get(field.getType()); field.set(bean, dependency); } } } }这个练习做完你对“类扫描—实例化—属性注入”的理解会非常通透。等回头再看Spring的源码会发现它的路径完全一致只是多了各种扩展点和循环依赖处理。4.2 循环依赖问题与三级缓存原理面试里高频的“Spring三级缓存”其实就是在处理循环依赖。先想一个问题A依赖BB又依赖A容器先创建哪个都会发现缺另一半。Spring怎么解决Spring维护了三个缓存Map。一级缓存存放完全初始化好的对象二级缓存存放早期暴露的半成品对象可能还没有完成属性注入三级缓存存放的是对象工厂。创建A时发现缺B就先创建一个A的早期引用放进三级缓存然后去创建BB创建时发现缺A直接从缓存中拿到A的早期引用注入B创建完成后A再从容器里把B注入进来。第三级缓存存在的原因更微妙如果A对象需要被增强比如被AOP代理早期暴露的对象和最终对象必须保持一致。工厂的作用就是让对象有机会被代理后再暴露出去。新手阶段不必把每个细节都吃透但至少要知道这个设计解决的实际问题是什么这比背结论重要一百倍。4.3 Spring中Bean的Scope单例与原型默认情况下Spring容器里的Bean是单例的全局只有一个实例。这个设计有两大好处节省内存、提高复用效率。但也意味着如果你在单例Bean里放了一个有状态的字段并发请求会互相干扰这正是很多人写的Controller出现数据串号的原因。举个真实场景。我在一个项目里见过有人把用户id存到了单例Service的成员变量里结果同时有两个用户请求A用户的数据串到了B用户页面。排查到最后就是因为Service是单例共享字段导致线程安全问题。解决方法是把有状态的数据放到方法参数或ThreadLocal里或者把Bean定义为原型Scope。Service Scope(prototype) public class RequestContextHolder { private String currentUserId; }用原型Scope时要注意每次getBean都会创建新实例频繁创建可能会带来额外消耗所以只在真正需要独立状态时使用。5. 从零写一个Spring实战业务用户注册与登录5.1 业务场景设计光会Hello World不叫会Spring。我带你做一个真实风格的小项目用户注册与登录包含密码加密、参数校验、数据库存储、登录会话管理。需求其实是很多企业内部培训系统里最常见的第一课。代码不会太长但它能覆盖IoC应用、事务处理、AOP切面、属性配置读取、外部组件引入这些高频场景。这个项目做完你基本就能理解一个Java EE企业级应用的骨架是怎么搭起来的。技术选型再做一次减法不用Spring Boot就用Spring Framework加原生JDBC操作数据库这样可以把你对Spring本身的理解和JDBC语法复习一次。等彻底跑通后再换成MyBatis或Spring Data JPA你会感觉一切都水到渠成。5.2 模块与代码结构先确定分层结构controller包接收参数、返回响应service包业务逻辑、密码加密dao包数据库访问entity包User实体config包数据源配置user表极其简单id、username、password_hash、created_at。密码不能明文存储我这边用SHA-256加盐的方式。下面是整个流程的代码骨架注意看依赖注入是怎么贯穿层层的Repository public class UserDao { Autowired private JdbcTemplate jdbcTemplate; public int insertUser(User user) { String sql INSERT INTO user (id, username, password_hash, created_at) VALUES (?, ?, ?, ?); String id UUID.randomUUID().toString(); return jdbcTemplate.update(sql, id, user.getUsername(), user.getPasswordHash(), new Date()); } public User findByUsername(String username) { String sql SELECT * FROM user WHERE username ?; ListUser users jdbcTemplate.query(sql, new BeanPropertyRowMapper(User.class), username); return users.isEmpty() ? null : users.get(0); } }JdbcTemplate是Spring对JDBC的轻量封装它把连接获取、PreparedStatement创建、结果集映射这些样板代码全部收走了。你只需要写SQL和关注业务逻辑。Service层负责核心逻辑Service public class UserService { Autowired private UserDao userDao; public void register(String username, String password) { if (userDao.findByUsername(username) ! null) { throw new BusinessException(用户名已存在); } String salt generateSalt(); String passwordHash Sha256Utils.hash(password salt); User user new User(); user.setUsername(username); user.setSalt(salt); user.setPasswordHash(passwordHash); userDao.insertUser(user); } public String login(String username, String password) { User user userDao.findByUsername(username); if (user null || !verifyPassword(password, user)) { throw new BusinessException(用户名或密码错误); } return TokenUtils.generateToken(user.getId()); } }这里我把盐值也存到了user表正式项目还需要考虑哈希算法迭代、加密盐生成强度等细节。入门阶段先把链路跑通。Controller层直接调用ServiceController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/register) ResponseBody public Result register(RequestBody RegisterRequest request) { userService.register(request.getUsername(), request.getPassword()); return Result.success(); } PostMapping(/login) ResponseBody public Result login(RequestBody LoginRequest request) { String token userService.login(request.getUsername(), request.getPassword()); return Result.success(token); } }我特别提醒一点除了Controller本身会标注这是一个处理器组件这个类还得启用组件扫描才有效。注解不是魔法没有扫描一切白搭。5.3 事务怎么配置最合理注册逻辑中如果插入用户头像信息、初始化用户配置等操作和插入user在同一个事务里任何一个失败都不能只留下半截数据。Spring的事务管理只需要加一个注解Transactional(rollbackFor Exception.class) public void register(String username, String password) { // 业务操作 }很多新手会问Service内部方法自己调用自己事务注解会不会失效答案是会。因为Spring的事务是通过AOP代理实现的外部调用会经过代理对象内部this.method()调用走的是原始对象切面逻辑根本不会触发。这也是为什么我们通常把事务放在Service的公开方法上跨类调用时才能被代理识别。踩坑记录一事务默认只在RuntimeException时回滚如果业务里抛出的是受检异常需要在注解里显式声明rollbackFor。踩坑记录二数据库表必须使用支持事务的引擎MySQL里MyISAM引擎不支持事务你注解加得再正确也不会有回滚动作。5.4 AOP切面登录鉴权的最佳拍档Spring里做登录鉴权的常见姿势也不是在每个Controller方法里手写校验而是用AOP。AOP全称面向切面编程它把那些横跨多个业务模块的通用逻辑比如日志、鉴权、参数打印抽成切面统一处理。一个极简登录校验切面可以这样写Aspect Component public class AuthAspect { Around(annotation(RequireLogin)) public Object checkLogin(ProceedingJoinPoint pjp) throws Throwable { // 从ThreadLocal或请求头中获取token校验失败则抛异常 return pjp.proceed(); } }这段代码的意义在于标记了RequireLogin注解的方法方法本身不需要关心登录校验切面会在执行前自动拦截处理。业务代码更干净鉴权规则的修改只动一个地方。AOP的底层是动态代理实现Spring目标类有接口时默认使用JDK动态代理没有接口时会用CGLIB生成子类代理。入门时知道这个区别就够了避免面试时被问到“为什么你的类加了切面却不生效”这类问题。6. 从Spring到Spring Boot再到Spring Cloud技术栈怎么延展6.1 Spring Boot到底“Boot”了什么Spring Framework本身很强大但配置繁琐。Spring Boot的出现本质上是把“约定优于配置”做到了极致内嵌Tomcat、自动配置、起步依赖管理、健康检查一个个痛点都被消化掉了。我经常打一个比方Spring是发动机Spring Boot是整车。发动机需要你自己接油路、电路、冷却液整车通电就能开走。你的Spring入门知识在为使用Spring Boot做铺垫你见过的那些RestController、Autowired、Transactional到了Spring Boot里全部无缝延续。换个角度说如果你直接学习Spring Boot却不理解IoC底层遇到自动配置失效时会非常困惑为什么别人的项目里数据源自动配好了我的却报找不到数据源因为你没搞懂自动配置背后的Conditional注解机制和SpringFactories机制。6.2 从单体到微服务Spring Cloud的使用场景现在很多Java EE企业级应用已经不再是简单单体而是拆成多个服务独立部署。Spring Cloud基于Spring Boot提供了一套微服务基础组件服务注册发现、配置中心、网关路由、熔断限流。但注意我建议你不要一开始就扎进微服务。一个需要掌握的判断标准是如果系统只有两三个模块、用户量不大强行拆微服务只会让分布式事务、链路排查、部署运维的复杂度飙升。技术选型不是越新越好而是匹配业务现状。6.3 Spring AIJava领域的新方向最近Spring AI热度很高它是Spring为AI应用提供的官方集成框架。可以简单理解为通过统一抽象把大模型接入到Spring生态里支持主流模型厂商的API能让你用熟悉的Bean方式配置模型客户端像调用Service一样调用AI能力。Java开发者多年的积累不会因为AI浪潮作废Spring AI就是那个衔接点。你可以先把聊天补全、提示词模板、结构化输出跑通后续如果要做智能客服、文档分析类需求再逐步深化。7. 常见问题与排查技巧实录7.1 高频报错速查表下面这些是我在辅导新人过程中遇到的真实高频问题每个都对应明确解法报错信息根因解决方案NoSuchBeanDefinitionExceptionBean没被扫描到或类上没有组件注解检查扫描包路径、检查Component/Service等注解NoUniqueBeanDefinitionException同一个接口有多个实现类容器不知道注入哪个用Primary指定默认或Qualifier按名称指定BeanCurrentlyInCreationException循环依赖无法解决检查构造器注入循环改成Setter注入或重新设计依赖UnsatisfiedDependencyException依赖的类型容器里不存在检查被依赖类是否注册为Bean、包路径是否正确ClassNotFoundException: jakarta.servlet项目在Spring 5时代引入了Servlet API但版本跑到了Spring 6统一使用Jakarta命名空间NullPointerException: field userDao对象是new出来的没有交给Spring管理注入没生效不要自己new改成从容器获取7.2 排查Bean装配问题的通用套路遇到“注入不进去”的问题我一般按三板斧排查。第一板斧确认配置类上有没有Configuration有没有启用ComponentScan。如果你用了SpringBootApplication注解它本身就包含了组件扫描但扫描范围默认是从启动类所在包向上向下。把启动类建在com.example.app业务类放在com.other.biz扫不到是必然的。第二板斧用调试模式启动或加启动参数让Spring打印Bean创建日志。可以用context.getBeanDefinitionNames()打印所有Bean名字对比一下你要找的对象是否存在。第三板斧检查是否有两个类名相同但包路径不同的Bean或者同一个接口有多个实现。遇到这种情况最简单的解法就是在实现类上标明组件名称再用Qualifier指定注入哪一个。7.3 哪些配置一定不要写在代码里入门阶段很多人图方便把数据库地址、用户名、密码直接写在数据源配置类里。这个习惯在个人练习中无伤大雅但到了企业级应用中是大忌。正确的做法是把配置外置到properties文件或用Spring的Environment读取系统环境变量。这样开发环境、测试环境、生产环境才能通过不同的配置复用同一套代码。Spring Boot里更推荐用application.yml配合profile做多环境配置这些可以在入门后尽快养成习惯。7.4 升级版本时的三个深坑版本升级是从Spring到Spring Boot过程中最容易翻车的地方。我总结三个深坑第一依赖版本不一致。Spring Framework和Spring Boot两个大版本的兼容关系需要严格匹配硬生生把5.3版本的项目升级核心库到6.0其他模块不升启动时大概率报NoClassDefFoundError。第二配置属性被移除或改名。Spring Boot 2.x升3.x时很多配置项前缀变了比如一些server.xxx调整到management端点相关配置里导致旧的配置不生效。第三嵌入式容器的线程模型变化。Spring Boot 3默认使用Tomcat 10底层实现了Jakarta Servlet对于老的HttpServletRequest写法要把javax整体替换成jakarta。这不是搜索替换那么简单有些第三方库也需要升级到兼容版本。句句都忘不了的实操心得最后再说一点个人体会。我带过的每个新手都会经历三个阶段第一个阶段觉得Spring难满屏的注解不知道从哪开始第二个阶段照着抄会用了但又不敢说自己懂第三个阶段开始琢磨源码、手写容器才能真正游刃有余。如果你现在处于第一阶段不要慌。我的建议不是让你立刻看源码而是把环境搭好、跑通一个又一个实例遇到问题就查、就打印日志、就打断点。我至今保留着一个习惯遇到奇怪的Bean装配行为先写一个最小复现代码单独跑一遍往往跑完就明白了。如果一定要给一个先后顺序我会说先用XML方式把Spring跑通再切注解先手写一遍微型容器再看三级缓存原理先把Spring单独走完再上Spring Boot先搞好一个可维护的单体项目再想微服务。这套顺序看起来慢但每一步都在给后面的快速学习铺路。每次聊到这个话题我总会想起自己改第一个Spring项目时的窘迫在配置里把DataSource类名拼错报错日志一大片翻了半天才发现就是个字母大小写的问题。如今再看那些报错已经能像读老朋友的信一样快速定位。技术这东西就是这样经验攒够了路就平了。
返回列表