1. 先回答那个最基础的问题为什么我们不再到处 new 对象我见过太多人学 Spring上来就背控制反转依赖注入这些概念背得滚瓜烂熟但问一句容器到底帮你做了什么立刻就哑火了。今天我想换个角度聊 Spring 容器不堆概念直接从代码的演变讲起。1.1 一个最朴素的例子订单服务的一天假设你正在写一个订单服务没有 Spring最原始的方式大概长这样public class OrderService { // 依赖商品库存、用户账户、消息通知 private ProductStockService stockService new ProductStockService(); private UserAccountService accountService new UserAccountService(); private MessageNotifyService notifyService new MessageNotifyService(); public void createOrder(Order order) { stockService.deductStock(order.getProductId()); accountService.deductBalance(order.getUserId(), order.getAmount()); notifyService.sendOrderCreatedMessage(order); } }看起来没什么问题对不对但只要你在这个项目里待上一两年就会踩到这几个坑每次new ProductStockService()的时候它的构造函数里可能又要new好几个别的服务层层嵌套牵一发动全身。如果某个服务需要被多个地方共享比如数据库连接池每个调用方各 new 一份资源瞬间就爆了。你想给某个服务加个缓存、加个日志代理得去改所有创建它的地方。单元测试的时候new出来的硬编码依赖根本没法替换成 Mock 对象。这几个问题归结起来就一句话对象的创建和组装逻辑不该由每个业务调用方自己负责。谁都不该关心我调用的那个服务到底怎么来的业务方只关心给我一个能用的服务。1.2 容器到底接管了什么Spring 容器做的事情本质上就是把你从自己造对象变成向容器要对象。当时所有的类都由容器统一创建、统一管理、统一装配业务代码只负责声明依赖关系剩下的交给容器。Service public class OrderService { private final ProductStockService stockService; private final UserAccountService accountService; private final MessageNotifyService notifyService; public OrderService(ProductStockService stockService, UserAccountService accountService, MessageNotifyService notifyService) { this.stockService stockService; this.accountService accountService; this.notifyService notifyService; } // 业务逻辑不变 }对比一下就能看出来依赖还是那些依赖但顺序变了不再是 OrderService 自己去new别人而是被别人容器把依赖注入进来。这就叫控制反转——对象创建的控制权从代码手里移交给了容器。控制反转之后前面说的几个痛点就都解决了依赖的创建过程集中在容器里替换实现只改配置或注解不动业务代码。默认单例管理同样的服务全项目只有一个实例资源不浪费。想给服务加横切逻辑事务、日志、鉴权容器可以在不改变业务类的前提下包装代理对象。测试时容器可以被 Mock 上下文替代注入什么依赖完全由测试代码说了算。理解了这个为什么后面所有关于容器的细节——Bean 生命周期、三级缓存、后置处理器——都会变得顺理成章因为你始终知道容器做这一切的目的把对象的创建、管理、装配从业务代码中彻底剥离出来。2. BeanFactory 与 ApplicationContext容器家族的两个核心成员Spring 不是一个容器而是一族容器。最低层的是一个叫BeanFactory的接口往上派生出了ApplicationContext再到你日常用的AnnotationConfigApplicationContext、ClassPathXmlApplicationContext。很多人傻傻分不清这几个东西其实只需要抓住一条主线BeanFactory 是骨架ApplicationContext 是骨架 增强。2.1 BeanFactory最底层的那个货架BeanFactory按字面理解就是Bean 工厂它定义了最核心的能力根据名字或类型获取 Bean判断某个 Bean 是否存在获取 Bean 的类型信息等等。核心方法就那么几个你能记住的其实就一个getBean()。BeanFactory factory new XmlBeanFactory(new ClassPathResource(beans.xml)); OrderService orderService factory.getBean(OrderService.class);这是最原始的容器形态XmlBeanFactory在多数的 Spring 版本里已经被废弃了但它代表的思想没有变BeanFactory 只负责两件事——注册 Bean 的定义BeanDefinition以及按定义生产 Bean 实例并缓存起来。它更像一个极简货架架子有了、货能放上去、也能取下来仅此而已。什么国际化、事件广播、资源加载、AOP 织入对不起BeanFactory 一概不管。所以在实际项目中你几乎不会直接操作 BeanFactory你接触到的都是它的子接口。2.2 ApplicationContext加满了增值服务的全功能容器ApplicationContext继承自BeanFactory在货架的基础上扩展了大量企业级功能。我随手列几个最常用的事件发布与监听容器内外可以发布自定义事件业务模块之间解耦通信比如订单创建后发布一个事件统计、通知、风控各自监听互不干扰。国际化消息支持通过MessageSource按 Locale 取文案不再需要自己写一堆 if-else 判断语言。资源加载抽象统一用Resource接口访问 classpath、文件系统、URL 等不同来源的配置。自动注册后置处理器像ConfigurationClassPostProcessor这种重要角色会在容器启动时自动注册并执行不需要你手动调用。环境与配置抽象通过Environment和PropertySource管 profile 和配置项后面 Boot 的application.yml体系就是建立在这上面的。实际使用中AnnotationConfigApplicationContext是最常用的一种 ApplicationContext。基于注解配置的项目里它就是那台真正运转的机器ApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); OrderService orderService context.getBean(OrderService.class);启动这行代码之后Spring 会扫描指定包、解析Configuration和ComponentScan、注册 BeanDefinition、实例化所有单例 Bean、填充属性、执行初始化方法……整套流程。你可以把它理解成容器启动的完整仪式。2.3 实际项目中到底选哪个直接说结论99% 的场景用 ApplicationContext 而不是裸的 BeanFactory。哪怕是做一个极其轻量的 Demo我也建议从AnnotationConfigApplicationContext起步因为它背后注册了一系列必要的后置处理器能让你少踩非常多 为什么我的注解不生效 的坑。BeanFactory 的价值不在用而在理解——当你手写一个迷你 Spring 容器后面我会专门写一节的时候你会发现你实现的其实就是 BeanFactory 那套能力。而 ApplicationContext 那些花哨功能本质上就是在这个核心能力外围套了一层又一层增强。理解了底层上层再复杂你也能一眼看穿。3. 从注册到销毁Bean 在容器里的完整一生很多面试题会让你简述 Spring Bean 的生命周期但如果你只是背那十几个阶段的名字没有真正理解每一步在干嘛遇到稍微变形的题就抓瞎。我建议把 Bean 的生命周期拆成注册、实例化、初始化、使用、销毁五个大阶段然后逐步往里填细节。3.1 注册阶段先把菜谱登记好容器不是凭空就知道有哪些 Bean 的。启动时它先要做的一件事是收集所有 Bean 的元信息也就是BeanDefinition。BeanDefinition 包含的东西很多Bean 的类名、作用域singleton 还是 prototype、是否懒加载、初始化方法名、销毁方法名、属性值、构造函数参数等——你可以把它理解为一张菜谱容器照着菜谱才能做菜。获取 BeanDefinition 有几种途径XML 配置时代XmlBeanDefinitionReader解析bean标签生成 BeanDefinition。注解时代ClassPathBeanDefinitionScanner扫描Component、Service、Repository等注解生成 BeanDefinition。Configuration类里的Bean方法由ConfigurationClassPostProcessor解析跟扫描组件殊途同归。这一步最容易被忽略的重点是这个阶段容器还没有创建任何实体对象它只是在登记菜谱。你现在理解为什么说 Bean 的生命周期从BeanDefinition 注册开始了吧因为一个 Bean 在被实例化之前必须先被认识。3.2 实例化与初始化阶段从反射到完整可用的过程等所有 BeanDefinition 都注册完毕容器开始逐个实例化。默认情况下非懒加载的单例 Bean 在容器启动时就会被创建。实例化本身用的是反射Object bean clazz.getDeclaredConstructor().newInstance(); // 或者带参构造器由容器根据依赖关系解析参数所以一个不被 Spring 管理的普通类只要有无参构造器理论上也能被视频反射创建出来。但真正的生命周期重头戏在实例化之后——属性填充和初始化。我用一个简化但完整的流程说明这个阶段背后发生了什么容器根据 BeanDefinition 找到构造器或工厂方法反射创建实例——这时候对象是裸的所有属性都是默认值。进行属性填充依赖注入如果某个属性被Autowired标注容器会递归去获取对应的依赖 Bean 并设置进去。执行 Aware 系列回调比如BeanNameAware、BeanFactoryAware让 Bean 能感知自己在容器中的身份。调用BeanPostProcessor的postProcessBeforeInitialization——这是初始化前的一个扩展点比如PostConstruct就是在这一步被处理的。调用初始化方法如果你实现了InitializingBean会执行afterPropertiesSet()如果你配置了init-method或使用Bean(initMethod ...)会执行对应的方法。调用BeanPostProcessor的postProcessAfterInitialization——这里最常见的就是 AOP 代理的创建Spring 会在这时包装出代理对象。走到这一步Bean 才算是完全体可以交给业务使用了。3.3 销毁阶段优雅地挥手告别容器关闭时会调用所有单例 Bean 的销毁方法。同样有三条路实现DisposableBean接口的destroy()方法、配置destroy-method、或者在Bean上标注destroyMethod属性。这里有一个容易踩坑的点prototype 作用域的 Bean容器是不负责销毁的。因为原型 Bean 每次获取都会创建新实例容器根本跟踪不过来。这个我后面会再展开说先记住这句话单例的善后工作容器全包原型的善后工作你自己负责。另外要提醒一句如果 Bean 实现了AutoCloseable或CloseableSpring 默认会把它识别为关闭方法。有时候你只是想关闭某个资源却发现附带的关闭动作被执行了两次多半就是没搞清楚这个默认行为。4. 三级缓存Spring 破解循环依赖的那张底牌如果说生命周期是容器的日常那么循环依赖就是容器最惊险的一次极限操作。网上讲三级缓存的文章很多但大多数只告诉你有三层 Map却不说清楚每一层到底在缓存什么、为什么非要三级。我先用最平白的语言解释清楚循环依赖是什么。4.1 场景复现A 需要 BB 需要 A假设有 A 和 B 两个类Service public class A { Autowired private B b; } Service public class B { Autowired private A a; }A 需要注入 BB 又需要注入 A这就是循环依赖。如果 A 必须先创建好才能给 B 注入而 A 又需要 B 才能创建好那就成了死循环。Spring 的解决办法是允许先创建一个半成品A把它放进缓存然后接着创建 B当 B 需要 A 时直接从缓存里拿到那个半成品 A 注入进去。这个思路本身很简单复杂在于——到底把半成品放在哪、放多久、怎么保证最终拿到的是同一个完整实例。4.2 每一级缓存都负责什么先看三个 Map这是三级缓存的物理载体一级缓存singletonObjects存放完整创建完毕的单例 Bean。这是最终对外暴露的实例。二级缓存earlySingletonObjects存放早期暴露的成品对象也就是已经实例化但还没完成属性填充或初始化的对象。注意二级缓存里放的是对象本身不是工厂。三级缓存singletonFactories存放ObjectFactory 工厂。每次从工厂getObject()时可能会产生不同的对象——这给了后置处理器一个包装代理的机会。完整流程我用一个例子走一遍创建 A检查各级缓存都没有 A。于是 A 开始实例化得到一个半成品 A属性还是空的。创建 A 时发现它依赖 B于是去获取 B。缓存里没有 B开始创建 B。创建 B 时发现 B 依赖 A此时 A 已经注册到三级缓存singletonFactories。B 从三级缓存里通过 ObjectFactory 得到 A 的引用可能是个代理也可能就是原始对象。B 拿到 A 的引用后属性填充完成继续初始化最终成为完整 B放进一级缓存singletonObjects。回到 A 的创建流程把 B 注入 A 中A 继续初始化最终成为完整 A放进一级缓存。4.3 为什么必须是三级而不是两级这是几乎所有面试官都会追问的深水区。两级行不行理论上如果只想要引用共享两级就够了——实例化后直接把原始对象放进一个缓存后续所有依赖方都拿这个原始对象。但问题在于如果 A 在初始化阶段被 AOP 代理了怎么办Spring 的 AOP 默认在postProcessAfterInitialization阶段生成代理对象。假设 A 最终要被代理那么早期暴露给 B 的那个原始 A和最终业务里使用的代理 A就不是同一个对象了。B 里拿着原始 A业务里用的是代理 A两个引用不一致后面所有依赖 A 的地方都会出问题。三级缓存里的singletonFactories就是为了解决这个时机错位。它保存的是 ObjectFactory这个工厂在执行时会让 A 的后置处理器有机会返回一个代理对象。换句话说没有三级缓存就无法保证循环依赖中提前注入的引用和最终暴露的引用是同一个。这也是为什么 Spring 在处理循环依赖时默认只在单例作用域下有效——单例的生命周期完全受容器掌控才有提前暴露 最终统一的余地。4.4 构造器注入为什么救不了循环依赖很多人踩过这个坑把Autowired从字段移到构造器上循环依赖直接报错。原因很清晰——构造器注入要求在对象创建的同时就传入依赖可 A 还没创建完你连半成品都没有三级缓存里的工厂根本还来不及注册。所以先暴露半成品再补依赖这套玩法对构造器依赖是无效的。我在项目里碰到循环依赖第一反应不是去追三级缓存的工作原理而是先想这个循环是不是设计上出了问题大部分情况拆掉一环就解决了——把其中一个依赖改成方法参数传递、用Lazy延迟注入、或者引入中间层。三级缓存是兜底方案不是让你肆意写环的许可证。5. 启动流程中的后置处理器容器里那些看不见的搬运工Spring 容器启动流程里有两个名字特别像、作用却完全不同的后置处理器一个是BeanFactoryPostProcessor一个是BeanPostProcessor。这两个名字在面试题里出现频率极高我每次带新人第一件事就是让他们把这两个词背清楚因为搞混它们整个容器原理就崩了。5.1 BeanFactoryPostProcessor在做菜之前改菜谱BeanFactoryPostProcessor在容器启动的早期执行执行时机是所有 BeanDefinition 已经注册、但还没有任何 Bean 被实例化的时候。所以它是拿来修改 BeanDefinition 的也就是我前面说的菜谱改改还能来得及。最有名的实现是ConfigurationClassPostProcessor它负责解析Configuration类、扫描ComponentScan、把Bean方法转成 BeanDefinition。另外还有PropertySourcesPlaceholderConfigurer负责把配置文件里的${jdbc.url}这类占位符解析成实际值。Component public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { BeanDefinition bd beanFactory.getBeanDefinition(orderService); bd.setScope(prototype); // 在实例化之前把作用域改掉 } }注意关键点这时候 beanFactory 里只有 BeanDefinition没有任何真实对象。你动的是未来怎么做的策略还不涉及已经做好的菜。5.2 BeanPostProcessor每个 Bean 的必经收费站BeanPostProcessor则不同它作用在每个 Bean 的实例化前后每个 Bean 创建时都得从它这里过一遍。它有两个方法postProcessBeforeInitialization和postProcessAfterInitialization。这里有个很多人忽略的细节BeanPostProcessor本身也是 Bean但它在容器里是提前被初始化的——容器会优先找出所有 BeanPostProcessor 类型的 Bean把她们准备好然后才开始批量创建其他 Bean。实际开发中最常见的 BeanPostProcessorAutowiredAnnotationBeanPostProcessor处理Autowired和Value注入。在属性填充阶段就是它去解析需要注入的依赖并完成赋值。CommonAnnotationBeanPostProcessor处理PostConstruct、PreDestroy等 JSR-250 注解。AnnotationAwareAspectJAutoProxyCreator处理Aspect切面在 Bean 初始化完成后创建 AOP 代理。如果说 BeanFactoryPostProcessor 是改菜谱的人那 BeanPostProcessor 就是每道菜出锅前都要检查的质检员。这两者一个是面向容器全局的一个是面向每个 Bean 的搞清了主语就不会再记混。5.3 它们在启动流程中的执行顺序把整个启动流程串起来是这样的一个顺序创建并初始化 ApplicationContext读取配置注册 BeanDefinition。执行所有BeanFactoryPostProcessor——此时可以修改 BeanDefinition。实例化并注册所有BeanPostProcessor——为后续每个 Bean 的创建做准备。逐个实例化单例 Bean走完实例化 → 属性填充 → 初始化前处理器 → 初始化 → 初始化后处理器这条链路。容器启动完成所有可用的单例 Bean 已经就绪。这个顺序解释了为什么你自定义的 BeanFactoryPostProcessor 一定要在属性填充前生效也解释了为什么 BeanPostProcessor 可以拦截所有 Bean——因为它注册得早而且容器在创建每个 Bean 的时候都会调用已注册的处理器。6. 手写一个迷你 Spring 容器把原理变成肌肉记忆看书看十遍不如自己写一遍。我当年彻底搞懂 Spring 容器就是靠照着思路从零手写了一个 200 行的迷你容器。这个迷你容器不搞 AOP、不搞事务只实现最核心的扫描、注册、注入、单例缓存但写完以后你再看 Spring 源码会有一种恍然大悟的感觉。6.1 第一步扫描并注册 BeanDefinition我用的方式是最简单的包扫描。遍历指定包下的所有 class 文件筛选出加了MyComponent注解的类生成一个简易的 BeanDefinition 注册表public class MiniApplicationContext { private final MapString, BeanDefinition beanDefinitionMap new HashMap(); private final MapString, Object singletonObjects new HashMap(); public MiniApplicationContext(String basePackage) { scan(basePackage); createSingletonBeans(); } private void scan(String basePackage) { // 伪代码示意遍历 classpath 下 basePackage 对应的目录 for (Class? clazz : findAllClasses(basePackage)) { if (clazz.isAnnotationPresent(MyComponent.class)) { String beanName lowerFirst(clazz.getSimpleName()); beanDefinitionMap.put(beanName, new BeanDefinition(clazz)); } } } }这里的关键是理解 BeanDefinition 的本质它就是一个这个 Bean 怎么造的元数据对象。真实 Spring 里它有几十个属性迷你版只需要保留Class?就行——因为后面可以反射拿到构造器和字段。6.2 第二步实例化与依赖注入扫描完 BeanDefinition接下来就是创建单例 Bean。创建的核心逻辑是反射实例化 → 按注解注入字段 → 放入单例缓存。private Object createBean(String beanName, BeanDefinition bd) throws Exception { Class? clazz bd.getClazz(); Object instance clazz.getDeclaredConstructor().newInstance(); // 依赖注入找出所有 MyAutowired 字段 for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(MyAutowired.class)) { String dependentBeanName lowerFirst(field.getType().getSimpleName()); Object dependentBean getBean(dependentBeanName); field.setAccessible(true); field.set(instance, dependentBean); } } return instance; } public Object getBean(String beanName) { // 单例缓存优先 if (singletonObjects.containsKey(beanName)) { return singletonObjects.get(beanName); } BeanDefinition bd beanDefinitionMap.get(beanName); Object bean createBean(beanName, bd); singletonObjects.put(beanName, bean); return bean; }光写到这里这个容器就已经能支持单例、支持最简单Autowired注入了。但请注意因为getBean()是递归进入的A 依赖 B、B 依赖 A 的循环依赖在这个朴素版本里会直接栈溢出。这正是为什么接下来要引入单例工厂缓存。6.3 第三步加上三级缓存处理循环依赖仿照 Spring 的做法我在类里加一个singletonFactories缓存。当实例化完一个 Bean、还没开始属性填充时就把一个能生成该 Bean 的 ObjectFactory 放进三级缓存。这样当循环依赖发生时对方能拿到半成品的引用public Object getBean(String beanName) { if (singletonObjects.containsKey(beanName)) { return singletonObjects.get(beanName); } if (earlySingletonObjects.containsKey(beanName)) { return earlySingletonObjects.get(beanName); } ObjectFactory? factory singletonFactories.get(beanName); if (factory ! null) { return factory.getObject(); } BeanDefinition bd beanDefinitionMap.get(beanName); Object instance instantiate(bd); // 提前暴露半成品放入三级缓存 singletonFactories.put(beanName, () - instance); // 属性填充进行依赖注入 populateProperties(instance, bd); singletonObjects.put(beanName, instance); singletonFactories.remove(beanName); return instance; }写到这里你会发现理解三级缓存的难点不在于那三行 Map 代码而在于哪些对象在什么时候放进哪一层。我这个迷你版本虽然省去了代理对象的复杂逻辑但已经足够让你感受 Spring 在循环依赖处理上的核心节奏先让工厂暴露半成品再慢慢把饭做熟最后统一换成成品。6.4 手写容器的收获写完这个迷你容器你会自然地产生几个疑问为什么真实 Spring 里有那么多扩展点为什么 BeanPostProcessor 时机那么重要为什么Configuration类的处理那么复杂这时候再回头看 Spring 源码眼里就不再是天书而是一层层可以拆解的套娃了。我把手写容器的过程录成过小课带过几个新人普遍反馈是之前背了二十遍生命周期都记不住自己写完一遍生命周期再也没忘过。真心建议每个 Java 后端都找个周末试试。7. 容器日常使用中的几个易踩坑点与我的经验讲完原理最后聊点实际工作中天天碰到的情况。这些坑不算深但每个都真实存在而且一旦触发排查起来特别费时间。7.1 同一个类有多份 Bean 定义该怎么拿当接口有多个实现类时getBean(SomeInterface.class)会直接抛NoUniqueBeanDefinitionException。常见处理方式有三种在其中一个实现类上加Primary告诉容器没特别指定就用我。用Qualifier(具体Bean名)精确指定。直接注入ListSomeInterface让容器把全部实现注入到一个集合里。我个人在项目里最常用第三种——配合策略模式特别顺手。你要实现一个支付场景支付宝、微信、银行卡各是一个实现直接注入 List然后根据类型去匹配调用比写一串 if-else 干净得多。7.2 单例与原型别被默认单例坑了单例是 Spring 默认作用域这也就意味着所有线程共享同一个 Bean 实例。如果这个 Bean 有可变状态比如一个实例字段用来存用户信息高并发下必然出事。我以前接过一个线上事故就是因为把用户会话塞进了一个单例 Service 的成员变量里结果 A 用户的操作被 B 用户的数据覆盖了。判断标准就一句话单例 Bean 里只放无状态逻辑和只读配置有状态数据一律放方法参数、ThreadLocal 或者干脆不存。如果你确实需要每次获取新实例把作用域改成 prototype但谨记前端说过的原型 Bean 不受容器销毁管理涉及资源清理你得自己兜底。7.3 启动慢、内存高先别急着怪容器很多人会问我Spring Boot 项目启动为什么越来越慢或者为什么容器内存占用那么高。这里有个常被忽略的方向容器启动时默认创建所有非懒加载单例 Bean你的项目 Bean 数量成百上千每个都要经历完整的实例化和初始化链路自然慢。排查思路一般是这样用spring.main.lazy-initializationtrue开启全局懒加载启动速度立竿见影代价是第一次访问某个 Bean 时有加载延迟。用 Actuator 的beans端点导出所有 Bean 清单找出哪些 Bean 其实根本没人用。内存高的问题先看看是否加载了海量不必要的自动配置类用debugtrue查看自动配置报告把用不到的排除掉。容器不是洪水猛兽但要学会按需裁剪。我现在接手老项目的第一个动作就是先导出 Bean 清单看看往往能发现一堆注册了但从来没人碰的僵尸 Bean。7.4 获取 Bean 的位置决定你的架构风格ApplicationContext可以在任何地方被注入但滥用ApplicationContext.getBean()是一种架构腐蚀的信号——它意味着业务代码又在主动找对象这违背了 IoC 的本意。我在代码评审里看到有人随手在 Service 里注入 ApplicationContext 去 getBean都会建议他改成构造器注入。让容器把依赖递到你手里而不是你去容器里淘金这样才能保证依赖关系清晰可见测试也更好做。我自己实际写代码的习惯是构造器注入为主Autowired字段注入只在特殊场景用能不用getBean()就尽量不用。这个习惯不是洁癖是长时间被依赖到底从哪来这个问题折磨出来的。关于 Spring 容器的理解最重要的其实不是记住某个 Map 的名字而是建立一条完整的链路认知配置驱动注册注册驱动实例化实例化过程里穿插后置处理器的扩展点扩展点之上再长出 AOP 和事务这些高级特性。有了这条线无论面试还是排障你都能顺着它一路摸下去。如果你正打算面试或者想彻底吃透容器我建议你按这个顺序做三件事第一把 BeanFactory 和 ApplicationContext 的关系画成一张图第二自己手动调试一次AnnotationConfigApplicationContext的refresh()方法跟着断点走一遍启动流程第三写一个迷你容器哪怕只支持注解扫描和字段注入都行。这三件事做完你对于 Spring 容器的理解会超过绝大多数只刷面试题的人。
企业数字化 ERP 产品动态
相关推荐
西门子PLC模拟量输入怎么接?4-20mA接线、数字量换算与数值异常排查 摘要:本文围绕西门子PLC模拟量输入,结合S7-200 SMART和S7-1200两类常用机型,系统讲解从标准信号、硬件接线、模数转换到工程量换算的完整链路。重点说明4-20mA、0-10V等标准信号的选择,两线制与四线制的接线要点,27648… · 2026/9/26 7:35:56
Atlas 300V 24G NPU推理卡详解:从环境搭建到YOLO部署实战 聊到“atlas”,在AI部署这个圈子里,百分之七八十的人指的都是华为昇腾的Atlas系列加速卡。我最近后台收到好几条私信,都在问“atlas 300v 24g 是运算加速卡吗”以及“atlas部署yolo怎么搞”。今天就把这张卡一次性说透:它到底算什… · 2026/9/26 7:35:56
基于MobileNet v2的口罩实时检测:从迁移学习到TFLite量化部署 简介:一份基于MobileNet v2的口罩实时检测系统完整实现资源,面向希望快速落地轻量级目标检测项目的开发者,也适合学习深度模型部署与Flask Web应用整合的入门者。系统内置实时视频流检测与图片上传检测两条功能链路:前者调用摄像头… · 2026/9/26 9:59:23
同样是AI工具,为什么国内放弃全局个性化?TaoToken统一Key配置实测 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 9:59:10
销售增长的关键,顾客满意度如何直接影响销售额 在数据驱动的世界中,能够有效地整合和分析来自不同系统和平台的数据至关重要。无论是个人购物记录、社交媒体互动,还是公司层面的销售与顾客反馈,分散的数据一旦汇总能提供深刻的洞察。尤其在商业领域,分析不同数据源之间的关系可以帮助公司更好地了解客户需求,优化产品和… · 2026/9/26 9:58:58
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46