ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Spring IoC与DI核心解析:从注解用到容器原理

Spring IoC与DI核心解析:从注解用到容器原理 先讲个我前两天真实遇到的事。团队里来了个新人我让他接一个模块改动他很快把代码撸完了类上贴了一堆Service、Autowired。我随口问他“你这段依赖是Spring怎么给你送进去的如果我有两个实现你想注入哪一个A依赖BB又依赖ASpring为什么还能正常启动”他愣了一下说“框架不是都处理好了吗”。这就是典型的Spring IoCDI只停留在“会用”层面的表现。说实话面试也好做Java EE企业级应用也好Spring IoC和DI都是躲不掉的硬骨头。它不像某个工具类记住了API就能用它是一整套设计思想决定了你的项目结构、代码耦合度、测试方式甚至是排查问题时的思路方向。这篇文章我打算结合自己多年的项目实践把 IocDI 的核心概念、实战用法、底层原理和各类面试考点揉碎了讲一遍尽量用大白话和能直接上手的示例让看完的你能从“会用注解”升级到“真懂容器”。1. 先把IoCDI的概念掰开揉碎很多人在IoC上栽跟头不是智商不够而是教科书写得太绕。什么“反转控制”“让容器管理对象”听着跟玄学似的。我用最直白的方式给你讲清楚。1.1 控制反转到底反了什么在没有Spring的年代我们写Java Web大多是下面这种玩法public class OrderService { private OrderDao orderDao new OrderDaoImpl(); public void createOrder(Order order) { orderDao.insert(order); } }看起来没啥问题但OrderService和OrderDaoImpl死死绑在一起。今天你想换一个OrderDao的Redis缓存实现得改OrderService的代码明天你想给OrderService写单元测试还得跟着new一个真实Dao出来Dao又依赖数据源测试成本直接翻倍。控制反转干的事就是把这段代码里的“new”权力收走。对象不是由使用方自己创建而是交给一个容器统一创建和装配使用方只需要声明“我需要一个OrderDao”容器就把合适的实例送过来。主动权从调用者手里反转给了容器这就是“控制反转”。我用生活里的事打个比方。以前你招待朋友从买菜、洗菜、炒菜、端盘全是你自己干这是传统开发。现在你打电话给餐厅订餐只说“我要一个宫保鸡丁”后厨怎么选食材、怎么调味、用什么盘子端上来你完全不关心这是IoC。餐厅就是Spring容器菜单就是你的依赖声明。1.2 依赖注入的三种姿势依赖注入是IoC落地的手段没有DIIoC就是空中楼阁。Spring里常用的注入方式有三种构造器注入Service public class OrderService { private final OrderDao orderDao; public OrderService(OrderDao orderDao) { this.orderDao orderDao; } }我个人非常推荐这种方式。好处是依赖在对象创建那一刻就固定下来字段能用final修饰整个对象要么完整创建成功要么创建失败不存在“注入一半”的中间状态。测试的时候直接new一个真实或Mock的OrderDao传进去就行不用借助Spring容器。Setter注入Service public class OrderService { private OrderDao orderDao; Autowired public void setOrderDao(OrderDao orderDao) { this.orderDao orderDao; } }这种方式适合可选依赖或者依赖需要运行时替换的场景。缺点是你无法确保对象在使用前一定被赋值如果忘了调用setter运行时容易空指针。字段注入Service public class OrderService { Autowired private OrderDao orderDao; }写起来最省事我见过大量项目整片整片都是这种写法。但它是把双刃剑依赖关系被隐藏了你看到的只有字段单元测试时不能只new一个对象完成注入必须配合Spring Test或者反射工具非常麻烦。而且字段被private包住IDE检查不友好容易出现“类能跑起来但依赖混乱”的坏味道。1.3 为什么Spring要这么设计理解IoC的意义不能只看“不需要new”这种表面好处。真正的原因有三个。第一是解耦。上层依赖抽象接口不依赖具体实现。接口不变底层实现随便换这就是后面Spring能衍生出AOP、事务管理、缓存抽象这些东西的地基。你想想一个多商户商城项目订单、支付、库存、物流、用户各个模块互相牵扯如果没有IoC做解耦整个系统改一处牵一发动全身。第二是可测试性。依赖由外部注入测试时往构造器里塞一个Mock对象不需要启动数据库、不需要走网络单测速度飞快。我接手老项目时最痛苦的就是大量类内部new出了各种依赖连个最简单的逻辑都没法单测。第三是生命周期管理。容器统一负责创建对象、做初始化、挂代理、执行销毁单例池、线程安全、事务拦截全部可以在对象创建过程中无缝织入。没有IoC容器这些横切逻辑根本无处安放。2. 从零到一IoCDI的实战用法概念讲再多不落地都是空谈。这一节我带你走一遍真实项目里的装配方式从最基础的注解扫描到Java Config再到外部化配置环环相扣。2.1 工程准备与第一行装配代码实践时我通常用Spring Boot搭骨架因为Boot的自动配置把最繁琐的那部分容器配置藏了起来你只需要聚焦业务。用IDEA新建一个Spring Boot项目引入Web依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency然后写一个最简单的接口和实现public interface UserDao { String findUserNameById(Long id); } Repository public class UserDaoImpl implements UserDao { Override public String findUserNameById(Long id) { return 用户 id; } } Service public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao userDao; } public String getUserName(Long id) { return userDao.findUserNameById(id); } } RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } GetMapping(/user/{id}) public String getUser(PathVariable Long id) { return userService.getUserName(id); } }启动应用访问/user/1你能看到容器把UserDaoImpl装配进UserService再把UserService装配进Controller。注意Controller里的构造器不是自己手动调的是Spring在启动阶段扫描到RestController发现构造器需要UserService就从容器里找到UserService的实例传进去。顺嘴提一个新手典型的坑IDEA创建Spring Boot项目失败或启动类找不到八成是JDK版本和Maven配置不匹配。建议JDK 8对应Spring Boot 2.xJDK 17对应Spring Boot 3.xMaven镜像源换成国内源启动速度会稳定很多。2.2 注解装配最常见也最容易用错Spring的IoC注解体系分两块注册Bean的注解和注入依赖的注解。注册Bean的注解有Component、Service、Repository、Controller。它们在功能上没有任何区别Spring扫描到它们都会把对应类注册为Bean。把它们分成四个名字纯粹是为了语义化Service告诉阅读者这是业务层Repository是数据层Controller是Web层。你去翻Spring源码会发现这四个注解本身都标注了Component。注入依赖的注解则是Autowired和Resource的重灾区很多人分不清。Autowired是Spring自己的注解默认按类型注入。如果同类型有多个Bean就结合Qualifier指定名字。Service public class OrderService { private final PaymentService paymentService; public OrderService(Qualifier(wechatPayService) PaymentService paymentService) { this.paymentService paymentService; } }Resource是Java EE时代留下的标准注解默认按名称注入名称找不到再按类型。如果你的项目从Java EE迁移到Spring或者有跨框架的诉求可以考虑它。但从纯Spring项目角度我一般建议统用Autowired语义更贴合Spring体系。再来看Value它用于注入外部配置Service public class AliPayService implements PaymentService { Value(${pay.alipay.app-id}) private String appId; }这里的${pay.alipay.app-id}会在容器启动时从application.properties或application.yml里读取。配置文件里写pay.alipay.app-idwx123456这个Bean的属性就会被自动赋值。顺便说一句很多初学者改端口号都去代码里找其实只要在配置里加server.port8081重启即可。2.3 Java Config把选择权握在自己手里注解注入虽然方便但也有力所不及的时候。比如引入第三方Jar包里的类你没法给它加Component。这时就需要Java Config登场。Configuration public class OssConfig { Bean public OssClient ossClient() { return new OssClient(endpoint, accessKey, secretKey); } }Configuration标记这是一个配置类Bean标注的方法会返回一个对象Spring会把方法返回值注册为容器中的Bean方法名就是Bean的名字也可以通过Bean(name)指定。后面任何地方需要OssClient直接注入即可。相比早期Spring的XML配置Java Config最大的优势是编译期检查。XML里的class属性写错只有启动时才报错Java Config里方法返回类型、参数类型都是强类型的写错了IDE立刻标红。可重构性也强类改名AltEnter一按全局跟着变XML里你还得手动改字符串。我维护遗留项目时最怕看到动辄几百行的XML配置。建议的落地策略是业务自己的类用注解自动扫描第三方依赖和需要定制构造参数的类用Java Config。两者可以混用以ComponentScan的扫描路径为界。2.4 复杂场景下的装配策略真实项目不会像教程那么清爽我挑几个高频复杂场景说说。同类型多Bean怎么选。一个系统不只一个支付渠道微信、支付宝、银联都实现PaymentService。这时候容器里有三个Bean直接Autowired会报NoUniqueBeanDefinitionException。正确的做法是给每个实现加明确的Bean名字注入处用Qualifier指定。还可以用Primary标出默认实现这样不想指定名字的注入点也能拿到主实现。用Import组织配置。当配置类多起来可以在一个入口配置上通过Import引入其他配置类让容器启动时的扫描入口保持整洁Configuration Import({OssConfig.class, DataSourceConfig.class}) public class AppConfig { }配置类里带条件装配。Spring Boot的自动配置大量用了条件装配比如ConditionalOnMissingBean表示“容器里没有这个Bean时才创建”。理解了这个你就明白为什么Boot能那么智能地按需装配各种组件。自己写通用模块时这套条件注解是控制装配时机的利器。3. 进阶必懂Bean生命周期与三级缓存如果说注解用法是IoC的外功那Bean生命周期和三级缓存就是内功。面试能不能镇住场子基本看这一块的深度。先声明一下Spring Boot 2.6之后默认关闭了循环依赖支持但源码里三级缓存机制依然存在下面讲的原理依然有效。3.1 Bean的一生从定义到销毁一个Bean从被容器感知到最终销毁大概走下面这些关卡解析Bean定义。Spring把带有注解的类或Bean方法解析成BeanDefinition里面记录了类的全限定名、作用域、初始化方法、属性值等。实例化前处理。InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation有机会在Bean真正创建前做手脚。AOP的Configuration类代理就与这个阶段有关。实例化。Spring通过反射调用构造器创建原始对象。此时对象还是一张白纸。属性填充。如果这个Bean依赖了其他BeanSpring会在这里完成依赖注入。构造器注入发生在实例化阶段字段和Setter注入发生在属性填充阶段。这个顺序差异正是后面循环依赖问题的根源。初始化前处理。BeanPostProcessor.postProcessBeforeInitialization执行比如PostConstruct标注的方法在这个阶段被调用。初始化。实现InitializingBean接口则调用afterPropertiesSet或者在XML/Java Config里指定initMethod。初始化后处理。BeanPostProcessor.postProcessAfterInitialization执行。Spring AOP的代理对象很多是在这个阶段生成的这也是三级缓存机制能兜住AOP场景的关键。使用。Bean进入单例池供整个容器使用。销毁。容器关闭时执行PreDestroy标注方法或调用DisposableBean.destroy方法。每次看这个清单我都要感叹Spring往一个对象的创建过程里塞进了太多扩展点。只要掌握了BeanPostProcessor你几乎能在Bean创建的任何环节插入逻辑MyBatis的Mapper代理、Feign的动态代理、事务的增强实现全是这么织入的。3.2 循环依赖与Spring三级缓存循环依赖是面试里绕不开的深水区。什么是循环依赖就是A的创建需要BB的创建又需要A。如果Spring不做任何处理A等B、B等A程序直接卡死。Spring用三张Map解决了这个问题一级缓存singletonObjects存放创建完成的完整单例Bean二级缓存earlySingletonObjects存放已经实例化但还没完成属性填充的早期Bean三级缓存singletonFactories存放ObjectFactory用来生成Bean的早期引用我画个场景推演A先开始创建实例化得到原始A对象。Spring把A的ObjectFactory放进三级缓存然后开始给A填充属性发现A需要B。容器继续创建B实例化得到原始B对象把B的工厂放进三级缓存接着给B填充属性发现B需要A。这时B去拿A一级缓存没有二级缓存也没有但在三级缓存里找到了A的工厂。工厂执行后返回一个A的引用这个引用被放入二级缓存B拿到A的引用完成自身创建并把自己放入一级缓存。A继续走B已经完整创建了A拿到B的引用完成自己的属性填充、初始化和代理增强最终放入一级缓存。这里就能看出问题A在属性填充阶段拿到的其实是自己的“提前暴露”版本不是最终完成代理增强后的对象。而A后面的整个初始化流程是在给自己补全最终一级缓存里存的后创建完成的A才是全量版本。这里面的引用关系Spring通过代理对象的提前引用做了处理细节比较多但核心思路就是你给我一个半成品先用着等我补全了你再补充后续的初始化。为什么一定要三级缓存而不是二级。这是高频追问。二级缓存也能解决字段循环依赖但没有三级缓存Spring无法优雅处理AOP代理。考虑A需要在创建完成后被代理如果B拿到的A是原始A对象那A后续创建的代理对象就和B持有的引用不一致业务逻辑就错乱了。三级缓存里存的是ObjectFactory它能在GetObject时延迟判断A需不需要代理如果不需要就返回原始引用如果需要就生成代理对象还能保证同一Bean只代理一次。正是这个“延迟决策”能力让Spring既保住循环依赖又保住AOP的正确性。哪些循环依赖救不了。一个是构造器注入的循环依赖因为构造器注入发生在实例化阶段A还没进三级缓存呢B创建时根本找不到A的半成品。另一个是prototype作用域的循环依赖原型Bean每次获取都是新实例Spring不缓存它们自然无从提前暴露。还有Async这类导致代理提前生成的场景处理起来更容易翻车最好的办法是从设计上消除循环依赖。我自己写代码的原则是字段循环依赖能用三级缓存兜底但绝不意味着你可以理直气壮地在项目里写出A依赖B、B又依赖A的烂代码。依赖关系应当是清晰向下的环状依赖本身就是设计的坏味道。3.3 作用域与后置处理器扩展Bean的作用域决定了容器的管理粒度。默认是单例singleton整个容器只有一份省内存适合无状态的Service、DAO等。prototype每次获取都新建适合有状态的任务类。Web环境下还有request、session、application三种作用域分别对应一次请求、一个会话、整个应用上下文。作用域使用最经典的坑是在单例Bean里注入原型Bean。单例Bean在容器启动时就创建一次注入的原型Bean也只有一份后续获取的永远是同一个和预期完全不符。解决办法是Scope配合ProxyMode或者用ObjectProvider延迟获取。Component public class SingletonBean { Autowired private ObjectProviderPrototypeBean prototypeBeanProvider; public PrototypeBean getPrototypeBean() { return prototypeBeanProvider.getIfAvailable(); } }ObjectProvider的好处是你不直接注入原型Bean实例而是注入一个“获取器”每次调用就触发一次容器的Bean查找从而拿到全新的原型实例。这个技巧在日常开发里能救很多次命。再聊聊BeanPostProcessor这是Spring的天花板扩展点。它能在Bean初始化的前后插入自定义逻辑。很多框架集成都靠它MyBatis的MapperScannerConfigurer扫描Mapper接口并生成动态代理注册成BeanSpring Security的MethodSecurityInterceptor通过后置处理器给Bean挂上安全切面。Component public class MyBeanPostProcessor implements BeanPostProcessor { Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof MyService) { System.out.println(MyService 初始化完成); } return bean; } }理解了这个机制你就明白Spring为什么叫“生态”而不是“框架”。所有能力都建立在IoC容器之上通过扩展点无限延展。4. 面试考点拆解从背诵到碾压这部分献给准备面试的朋友。面试官问IoC很少只问定义他更爱连环追问。我把高频问题整理成了链式问答方便你顺着思路组织答案。4.1 高频连环问题与回答思路问说一下你对Spring IoC的理解。常规回答“IoC就是控制反转把对象的创建和依赖交给容器管理。”这种回答及格但不出彩。我建议的回答是先提本质——对象创建权从调用者手中转移给容器DI是其实现手段再举业务场景——Service不依赖DaoImpl的具体类型只依赖抽象接口容器按声明注入最后补一句设计意义——解耦、可测试、统一生命周期管理。这样既体现理解又体现实战。问IoC和DI是一回事吗不是。IoC是设计思想DI是实现思想的方式之一。Spring用DI这套机制实现了IoC的效果。打个比方IoC是“客人不自己做饭”DI是“客人通过菜单告诉服务员自己想吃什么后厨做好端上来”。不用DI也能实现IoC比如用Service Locator模式但Spring选择了DI。问Spring里的Bean默认是单例的为什么这么设计因为大部分Service、Dao这类对象是无状态的它们内部没有可变字段同一时间可以被多个线程并发复用单例能极大减少对象创建的开销。Spring容器作为企业级应用的基础设施启动时就要扫描、解析、注入大量Bean要是每个Bean每次获取都新建一个性能和内存都撑不住。同时Spring通过ThreadLocal、方法参数等机制保证无状态Bean的并发安全。问Autowired和Resource有什么区别我会从几个维度回答Autowired是Spring的注解Resource是Java EE的标准注解Autowired优先按类型装配多个同类型时靠Qualifier按名字补充Resource优先按名字装配名字找不到再按类型Autowired默认要求依赖必须存在可以通过requiredfalse放宽Resource没有这个属性。最后补一句项目建议建议Spring项目统一用Autowired跨框架则考虑Resource。问Spring如何解决循环依赖把三级缓存的流程讲清楚重点回答“为什么是三级不是二级”参考第3.2节。加分项是主动说出哪些情况下循环依赖解决不了构造器注入、原型作用域、以及新版本Spring Boot默认禁止循环依赖的事实。再补一句个人实践经验靠缓存兜底不如靠设计消灭循环依赖。问容器启动时IoC和AOP的执行顺序是什么这是区分“背过答案”和“真正理解”的经典题。Spring容器启动时先解析Bean定义创建Bean属性填充并完成初始化然后BeanPostProcessor在初始化后阶段生成AOP代理。换言之IoC负责把Bean创建好AOP在Bean准备就绪后对方法做增强两者通过后置处理器衔接。事务管理、MyBatis的Mapper代理都是这么织进去的。4.2 手写一个迷你IoC容器编程面试现在越来越喜欢让候选人手写一个微型IoC。你不需要真把Spring写一遍但核心骨架要能自洽。我的实现思路分四步第一步扫描包下的所有类筛出带Component的类第二步用反射实例化存入一个Mapkey是Bean名字value是实例第三步遍历所有Bean解析字段上的Autowired从Map里找依赖并递归注入第四步提供getBean方法暴露对象。下面是我压缩后的核心代码Component public class OrderService { // 模拟需要注入的依赖 } public class MiniApplicationContext { private final MapString, Object singletonObjects new ConcurrentHashMap(); public void scan(String basePackage) throws Exception { String path basePackage.replace(., /); EnumerationURL urls Thread.currentThread().getContextClassLoader() .getResources(path); while (urls.hasMoreElements()) { URL url urls.nextElement(); File dir new File(url.toURI()); for (File file : dir.listFiles(f - f.getName().endsWith(.class))) { String className basePackage . file.getName().replace(.class, ); Class? clazz Class.forName(className); if (clazz.isAnnotationPresent(Component.class)) { String beanName clazz.getSimpleName(); Object instance clazz.getDeclaredConstructor().newInstance(); singletonObjects.put(beanName, instance); } } } injectDependencies(); } private void injectDependencies() throws IllegalAccessException { for (Object bean : singletonObjects.values()) { for (Field field : bean.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { field.setAccessible(true); Object dependency singletonObjects.get(field.getType().getSimpleName()); field.set(bean, dependency); } } } } public T T getBean(ClassT clazz) { return (T) singletonObjects.get(clazz.getSimpleName()); } }这段代码没有处理循环依赖也没有AOP但已经具备IoC容器的三要素统一创建、集中存储、按需注入。面试时能写出这个骨架再补一句“完整的Spring容器在此基础上增加了BeanDefinition解析、多级缓存、后置处理器和复杂依赖注入”会显得特别扎实。4.3 面试回答的避雷与加分第一个雷背定义而不懂设计。答IoC时只抛出“控制反转”四个字面试官一听就知道你不理解为什么。第二个雷把Autowired和Resource混为一谈。我建议你不仅要知道区别最好能说出Resource属于javax.annotation和jakarta.annotation的时代变迁这能体现你的Java EE背景宽度。第三个雷说Spring三级缓存是为了解决性能问题。三级缓存和性能没有直接关联它的本质是解决创建期依赖查询和AOP代理延迟生成问题。答错方向前面的印象分全丢。加分方式主动指出Spring Boot 2.6后默认禁止循环依赖。这传递一个信号你不光看了Spring的旧机制还关注了最新版本的行为变化。接着补一句“我在项目里用构造器注入消除循环依赖”面试官基本点头。5. 实务中的坑与排查经验这部分是纯经验分享。IoC用得好项目清爽用得糙排查问题能让你怀疑人生。5.1 常见问题速查表现象原因解决办法注入的Bean为null类没被Spring扫描到检查ComponentScan的包路径确保类所在包被覆盖NoSuchBeanDefinitionException容器里根本没有对应类型检查类是否加了注册注解或Bean方法是否执行NoUniqueBeanDefinitionException同类型存在多个Bean用Qualifier指定名称或Primary设置主实现UnsatisfiedDependencyException构造器需要某个依赖但找不到查看异常链路中具体缺哪个Bean补配置或补注解循环依赖启动失败构造器注入导致死锁改成Setter/字段注入或重新梳理依赖关系单例Bean里的原型Bean不生效注入的是固定实例用ObjectProvider或Scope(proxyMode ScopedProxyMode.TARGET_CLASS)5.2 排查思路与实用工具遇到IoC相关的诡异问题我有个固定的排查顺序。第一步看启动日志。把日志级别调到DEBUGlogging: level: org.springframework.beans.factory: DEBUG org.springframework.context: DEBUG启动时会打印“Creating shared instance of singleton bean xxx”“Autowired annotation processor”等详细信息能看到每个Bean是在哪个阶段出的问题。这个办法解决了我至少一半的定位难题。第二步使用IDEA的Spring辅助窗口。IDE左侧会出现Spring标签页里面能看到启动时扫描到的所有Bean、它们之间的依赖关系图。Bean被注入了没有、注入的是哪个实现一目了然。我经常在排查NoUniqueBeanDefinitionException时直接把Bean选中右键转到声明处效率极高。第三步看堆栈往里钻三层。很多依赖注入的异常信息很绕比如BeanCreationException包了好几层核心错误在后面。你看到Caused by那一行才是真正的根因别在前面打转。5.3 写了这么多年Spring我的几条心得第一能用构造器注入就不要用字段注入。我前几年带的项目里老代码全是字段注入结果Bean和Bean之间的依赖关系像蜘蛛网一样理不清。后来强制推行构造器注入每个类的依赖清单一目了然代码评审时扫一眼构造器就能看出这个类干了多少事。第二被Spring的“便利”迷惑不等于正确。自动扫描、自动装配省事了但也把依赖关系隐藏在暗处。我见过一个团队在Service里注入了几十个字段整个类臃肿得像上帝对象。IoC只是负责给你送东西不负责约束你该要多少。该按职责拆分时还是要拆分。第三循环依赖是设计警钟而非功能开关。三级缓存的存在让Spring看起来无所不能但你的架构里如果频繁出现环状依赖第一步该想的不是怎么配置让它跑通而是这里的依赖设计是不是出了问题。把公共逻辑抽出去让依赖方向变清晰比任何配置技巧都值钱。第四善用ObjectProvider解决可选依赖。当你需要某个Bean但它可能在容器里不存在时直接Autowired(required false)是一种解法但每次判空麻烦。用ObjectProvider优雅得多既能判断是否存在又能延迟获取配合默认值实现代码干净不少。写在最后讲完这一圈我对Spring IoCDI的印象可以用一句话总结它不是让你偷懒不用new而是逼你把代码结构想清楚。很多人在IoC上栽跟头其实不是学不会而是没搞懂它解决的到底是哪一类工程问题。如果你能把文章里那个迷你容器亲手写一遍把三级缓存那张缓存表在纸上推演一遍再回去看看自己项目里的Service和Dao是怎么纠缠在一起的你会突然发现眼前那些注解都变成了看得见摸得着的机制。这也是我写这篇文章的初衷Spring再花哨底层还是那些朴素的道理。把这层窗户纸捅破后面学AOP、学事务传播、学Spring Boot自动配置都会顺畅很多。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表