戏人看戏博客网，揭秘互联网真相戏人看戏学无止尽，教大家各种网络技术分享个人生活，给大家带来无限体验！
前言：项目管理上有一个著名的概念叫着「钻石依赖」，是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突。我们平时使用的 maven 是这样解决钻石依赖的，它会从多个冲突的版本中选择一个来使用，如果不同的版本之间兼容性很糟糕，那么程序将无法正常编译运行。Maven 这种形式叫「扁平化」依赖管理。我以我的这个项目为例，可以发现一个Spring的默认的BeanFacotry的接口，重复实现了呢4遍（实际上4个都是同样的，因为多引用了3个maven工具包导致Springboot 包被重复引用了）。这时候就要排查引入的Maven包的问题，因为这个3个工具包也是我自己实现的，然后推送到了Maven中央仓库就可以自己在项目中引入使用了。1.看问题现在来看问题看一下Maven包里的引用操作进来发现！我去，这还得了？我只引用你这个工具，结果你自己给我引入一堆spring-boot的包，并且还给我制定了版本！那我要是用的Spring-boot 3这不炸了？赶紧解决(我开源的扳手工具地址：https://github.com/nb-sb/whn-wrench)2.解决方案a.自己在项目中排除依赖b.修改扳手项目中的依赖scope类型(因为这扳手项目也是我自己实现的，所以为了通用型肯定直接修改扳手项目是最好的，但是如果你引入的项目是其他人的包，他的包引入的依赖和你的依赖冲突的，就要使用a办法，排除依赖，这里就不过多阐述了，网上很多教程)首先先修改scope范围，改为provided可以看一下scope可以填写哪些参数（这里不过多阐述，这篇主要来解决钻石依赖问题的）作用域编译classpath运行classpath测试classpath打包到产品compile✅✅✅✅provided✅❌✅❌runtime❌✅✅✅test❌❌✅❌system✅✅✅✅然而看后果结果还是没有完全排除，还是可以引入进来？什么鬼？继续排查经过不懈查找，破案了，原来是这个组件引入了Redission，Redission又依赖了spring-context-support，导致的重复引用。由于引用的版本和我项目版本一样所以没有异常，如果引入的版本和我项目的版本不一致，包不全会有依赖冲突问题OK，发现问题就好解决了，直接排除这个redission中的spring的冲突的依赖查看perfect ！！！没有重复引入的问题了结尾：提交代码到dev分支。。在合并到main分支 ->推送到Maven中央仓库总结：遇到钻石依赖一般可以直接排除，但是这里由于依赖的包是我自己写的，所以直接可以修改原包即可，大家如果遇到钻石依赖问题可以按照我这个思路排查1.查看依赖所在的pom文件2.分析版本问题所在的包中3.常见解决思路：排除依赖、版本锁定原则（定义指定版本）、父子模块覆盖原则（父模块里声明的依赖,子模块里重复声明。子模块内会覆盖父模块的依赖）
线程池中线程异常后：销毁还是复用？
前言下面测试是使用的JUC包下的 java.util.concurrent.ThreadPoolExecutor，本文将围绕验证，阅读源码俩方面来解析这个问题。这里使用两种提交方式分别验证，两种提交方式不一样结果也不一样！如果连这个都不知道，别跟我说你用过线程池好吗？如果线上出bug异常被吞掉，不知道怎么排查你就老实了。。1.验证execute提交线程池中public class ThreadPoolExecutorDeadTest { public static void main(String[] args) throws InterruptedException { ExecutorService executorService = buildThreadPoolExecutor(); executorService.execute(() -> exeTask("execute")); executorService.execute(() -> exeTask("execute")); executorService.execute(() -> exeTask("execute-exception")); executorService.execute(() -> exeTask("execute")); executorService.execute(() -> exeTask("execute")); Thread.sleep(5000); System.out.println("再次执行任务======================="); executorService.execute(() -> exeTask("execute")); executorService.execute(() -> exeTask("execute")); executorService.execute(() -> exeTask("execute")); executorService.execute(() -> exeTask("execute")); executorService.execute(() -> exeTask("execute")); } public static ExecutorService buildThreadPoolExecutor() { return new ThreadPoolExecutor(5, 10, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), new ThreadFactoryBuilder().setNamePrefix("test-").build() , new ThreadPoolExecutor.CallerRunsPolicy()); } private static void exeTask(String name) { String printStr = "[thread-name:" + Thread.currentThread().getName() + ",执行方式:" + name + "]"; if ("execute-exception".equals(name)) { throw new RuntimeException(printStr + ", 我抛异常了"); } else { System.out.println(printStr); } } }运行后发现，test-2的线程不见了，多出来一个test-3线程结论：execute 提交到线程池的方式，如果执行中抛出异常，并且没有在执行逻辑中catch，那么会抛出异常，并且移除抛出异常的线程，创建新的线程放入到线程池中。2.验证submit提交线程池中public static void main(String[] args) throws InterruptedException { ExecutorService executorService = buildThreadPoolExecutor(); executorService.submit(() -> exeTask("execute")); executorService.submit(() -> exeTask("execute")); executorService.submit(() -> exeTask("execute-exception")); executorService.submit(() -> exeTask("execute")); executorService.submit(() -> exeTask("execute")); Thread.sleep(5000); System.out.println("再次执行任务======================="); executorService.submit(() -> exeTask("execute")); executorService.submit(() -> exeTask("execute")); executorService.submit(() -> exeTask("execute")); executorService.submit(() -> exeTask("execute")); executorService.submit(() -> exeTask("execute")); }运行结果，把异常吞掉了，外面一点感知都没没有！并且线程也继续复用了，这要是出bug了得排查多久？结论：submit 提交到线程池的方式，如果执行中抛出异常，并且没有catch，不会抛出异常，不会创建新的线程。3.跟源码找原因这还得了？要是我用了这个，线上出bug不完蛋啦？赶紧看看源码，看看什么原因！ 3.1 查看submit执行方法java.util.concurrent.AbstractExecutorService#submit(java.lang.Runnable)这里submit原来是包装了一层RunableFutrue然后调用了execute方法！3.2 查看execute方法的执行逻辑 java.util.concurrent.ThreadPoolExecutor#runWorker看看异常处理方法是啥可以发现，如果抛出异常，会移除抛出异常的线程，创建新的线程。3.3 为什么上面刚开始测试的submit方法，没有创建新的线程，而是继续复用原线程？还记得，我们在3.1的时候，发现submit也是调用了execute方法，但是在调用之前,包装了一层 RunnableFuture，那一定是在RunnableFuture的实现 FutureTask中有特殊处理了，我们查看源码可以发现。原来如此！把异常自己吃了，放到ex变量里，等你get拿的时候给你跑异常，抛到主线程！所以，只要使用的是submit，可以通过java.util.concurrent.FutureTask#get()，就可以获取对应的异常信息。这时候不知道你有没有发现什么问题？是不是把异常抛出了，结果主线程中其他的操作也没法执行被影响了？这时候就要根据业务进行优化了（自己去想，别等我给你写。。。）。好了，这次的分享先到这里，最近事情比较多，更新博客速度都变慢了，哈哈哈总结一下：当一个线程池里面的线程异常后:当执行方式是execute时,可以看到堆栈异常的输出，线程池会把这个线程移除掉，并创建一个新的线程放到线程池中。当执行方式是submit时,堆栈异常没有输出。但是调用Future.get()方法时，可以捕获到异常，不会把这个线程移除掉，也不会创建新的线程放入到线程池中。
哈哈哈，每天都有垃圾评论，系统自动过滤掉了，不好意思，之前因为没有垃圾评论过滤和审核好多人发广告，不好意思哈～现在这个站点是前台是伪静态，缓存30天到腾讯云CDN EO节点上，所以可能时效性比较低，比如浏览量、评论(需要我手动审核)等，好处就是访问速度非常快，资源全在CDN上，而且是伪静态的，查询直接缓存到CDN上不走数据库。这样一些小黑子之前打我网站的，现在打不动了哈哈哈，DDOS找不到我原IP，CC根本打不动，因为伪静态查询不走数据库全打CDN上了，对我一点影响没有。打的量全被清洗了哈，脚本小子别费力了
前置知识：下面先用一个从ApplicationContext中getBean的一个案例熟悉一下用OkHttpClient注入到了ICO容器中并获取为例打断点进来发现，如果是无参构造的话则会执行if里面的，protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException { // 1. 解析 beanName（去掉别名，转为真正的名字） String beanName = this.transformedBeanName(name); // 2. 尝试从单例池里拿（一级缓存） Object sharedInstance = this.getSingleton(beanName); Object beanInstance; if (sharedInstance != null && args == null) { // 说明这个 Bean 已经创建过了（缓存命中） // 如果是循环依赖提前暴露的 Bean，这里会打印 trace 日志 if (this.logger.isTraceEnabled()) { if (this.isSingletonCurrentlyInCreation(beanName)) { this.logger.trace("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference"); } else { this.logger.trace("Returning cached instance of singleton bean '" + beanName + "'"); } } // 3. 如果是 FactoryBean，还需要处理 getObject() 包装 beanInstance = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition)null); } }可以发现sharedInstance和beanInstance指向了同一个地址如果下次进行getBean的时候，并且这个Bean对象设置原型模式，就会新疆一个Bean，否则则直接返回原来的对象Ioc容器加载过程：1. new ApplicationContext(配置类/xml)； @SpringBootApplication @Configurable public class Main { public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class); Object bean = applicationContext.getBean("IService"); } } //这里进行new一个ConfigurableApplicationContext public class SpringApplication { public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return (new SpringApplication(primarySources)).run(args); } }2. 解析配置--->扫描包--->bean的定义信息读取进来--->BeanDefinition-->mapBeanDefinition = 把类的配置信息登记到 Spring 容器（BeanFactory）的“元数据字典”里，方便后续创建 Bean。BeanDefinition，里面写着：beanClass = UserServicescope = singletonlazyInit = false3.循环一个个创建bean--->条件判断--->懒加载的bean\多例Bean\抽象bean条件判断的时候会判断是否是单例Bean，是单例Bean的话进行一个Bean对象的创建，因为BeanFactory#getBean4. 通过beanfactory.getBean创建bean对象创建之前先获取5.实例化 -> 在ApplicationContext中，通过调用getBean(Spring boot中是通过BeanFactory拿到)拿到BeanDifinitionMap通过name拿到这个Bean的反射射new AAService()6.DI 属性注入 （@autowrited 属性注入是进行注入一些属性，比如拿到字段上含有@autowrite注解的字段，进行getBean获取到后通过反射设置进去。）7.0 初始化前 BeanPostProserssor.postProcessBeforeInitialization7.初始化 （初始化后才算完整的Bean对象，回调一些方法）-> 如果你先调用自己的初始化方法，或者其他回调方法，都是在这里进行调用的，因为没有初始化的时候这个类的属性都没注入进来，自然而然也无法进行调用其他方法。回掉方法可以使用@Construct注解或者implemnt BeanPostProcessor重写里面的回调方法来实现。7.1初始化后 -->postProcessAfterInitialization--> AOP --> 动态代理对象查看一下BeanPostProserssorInitialization实现类 AbstractAutoProxyCreator#postProcessAfterInitialization继续往下看里面的创建代理对象的方法，这个实现原理实际上就是根据AspectJ中的切点表达式，进行匹配，比如这个切点@Pointcut里面的表达式，如果当前的类满足切点表达式的话，则可以执行if中的逻辑，进行执行createProxy创建一个代理对象@Aspect @Component public class LogAspect { // 切点表达式：拦截 com.example.service 包下所有类的所有方法 @Pointcut("execution(* com.example.service..*(..))") public void serviceMethods() {} // 在方法执行前打印日志 @Before("serviceMethods()") public void beforeServiceMethod() { System.out.println("【AOP】调用 Service 方法之前..."); } }继续往下看createProxy这个创建代理对象的源码继续往下看，看他怎么拿到代理对象的可以看到他的这个AopProxy代理对象的实现类（我这个项目引用了第三方的maven包没有进行排除，实际上就2个实现类，一个cglib一个jdk的动态代理）先看一下JDK的动态代理的实现（一目了然，能看到是使用了一个代理类进行创建代理对象的），CGlib也是差不多的就是创建一个代理对象这里我写一个简单的创建代理对象的例子，了解一下代理对象是什么public interface UserService { void addUser(String name); } public class UserServiceImpl implements UserService { public void addUser(String name) { System.out.println("添加用户：" + name); } } public class MyInvocationHandler implements InvocationHandler { private final Object target; public MyInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("调用前：日志记录"); Object result = method.invoke(target, args); // 调用真实方法 System.out.println("调用后：方法执行完成"); return result; } } // 使用 UserService target = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new MyInvocationHandler(target)); proxy.addUser("Tom"); //输出 调用前：日志记录 添加用户：Tom 调用后：方法执行完成 8.缓存起来 ->放到一级缓存singletonObjectsgetBean() --> 动态代理对象 -->调用方法时被增强只有实现了AOP的获取的才是代理对象AOP-> 动态代理Spring动态代理：JDK。 bean实现了接口使用JDK. 代理对象：动态生成实现目标bean接口的代理对象CGlib。 没有实现接口使用cglibJDK 代理对象 implment IAService{ add(){ // 前置通知 //调用你的目标方法 AService.add() //后置通知 } } CGlib 代理对象 extends 目标bean { // 重写目标方法 }QA：1.一级缓存能不能解决循环依赖能（如果只有一级缓存，那么锁粒度就很大，要锁整个getBean方法）2.二级缓存能不能解决循环依赖能（二级缓存可以降低锁的粒度）二级缓存 确实可以拿到“半成品 Bean”，解决最基础的循环依赖。虽然spring现在看是三级缓存解决的循环依赖，实际上二级缓存也可以解决，但是用二级缓存解决的话会导致代理对象注入错误（也就是只能返回原始 Bean，如果这个 Bean 需要 AOP 增强，就会导致依赖方注入的不是代理对象，增强逻辑失效）。所以需要三级缓存3.三级缓存是解决aop创建代理对象问题吗？（如果循环依赖时，注入了原始对象，后面再生成代理对象，就会出现 注入的不是最终的 Bean 的问题。）三级缓存这里有个误区，好多人以为三级缓存就是用来解决循环依赖的的，实际上只有一级缓存或者二级缓存都可以解决循环依赖！三级缓存还是主要用来规范Bean创建的生命周期的，如果没有三极缓存，那么Bean就必须要在实例化的时候创建代理AOP对象，加入三级缓存的时候让其只有在循环依赖的时候才会创建动态代理，保证他的规范性不全是，可以看到三级缓存他缓存的是getSingleton() —— 获取 Bean 时用三级缓存// 逻辑就是： 一级缓存 → 二级缓存 → 三级缓存（工厂生成） @Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 1. 先从一级缓存取（完全实例化好的 Bean） Object singletonObject = this.singletonObjects.get(beanName); // 2. 如果没有 && 正在创建中 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 2.1 尝试从二级缓存取（半成品 Bean） singletonObject = this.earlySingletonObjects.get(beanName); // 2.2 如果还没有 && 允许提前暴露 if (singletonObject == null && allowEarlyReference) { // 2.3 从三级缓存取 ObjectFactory，调用它创建早期引用（可能是代理对象） ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); // 放进二级缓存 this.earlySingletonObjects.put(beanName, singletonObject); // 删除三级缓存里的工厂 this.singletonFactories.remove(beanName); } } } } return singletonObject; }再往下看可以看到一个往3级缓存中put内容的方法，put进去的内容是一个 **ObjectFactory** 工厂在 AbstractAutowireCapableBeanFactory#doCreateBean 里，调用的DefaultSingletonBeanRegistry#addSingletionFactory方法。这里可以看到给三级缓存中原来存的是一个匿名函数！迎刃而解了，RootBeanDefinition看一下可以大概猜到了，存的相当于是告诉你创建哪个类的，这些累的基本信息都有了，直接反射就可以创建a.三级缓存还可以 支持代理对象（AOP、事务）在循环依赖场景下的正确注入，也就是说让代理对象都是在实例化后创建的初始化 判断 只有在循环依赖中bean，才在循环依赖中提前创建AOP代理对象，防止aop对象被注入的不是代理对象，也就说是如果一级缓存里没有bean对象，但是二级缓存中没有，这时候要用到三级缓存(实际上三级缓存存的是一个函数式的接口用来存一个bean对象)拿到这个aop Bean对象存到二级缓存中b.解决循环依赖（提前暴露半成品 Bean）4.Spirng怎么解决多级缓存循环依赖问题？⚠️注意：这个和第6个问题不一样！这个是单例Bean，第6题是多例bean使用三级缓存（就是第三级缓存，而不是三个缓存！）@Component class A { @Autowired B b; } @Component class B { @Autowired A a; }注意这里提示了循环依赖报错，需要手动添加配置文件，Spring Boot 2.6.x 开始，默认就关闭了循环依赖的支持，加上配置后就可以了spring.main.allow-circular-references=true注意这里有个小提示，a.如果Bean 被 AOP 代理了，就需要加上@Lazy注解如果 A 或 B 上有 @Transactional、@Async、@Validated 之类的注解，Spring 会给它创建代理对象在循环依赖过程中，Spring 可能没法把“代理对象”提前暴露出去 → 导致失败b.作用域不是单例 ，比如是圆型模式@Scope("prototype") 那 Spring 就不会缓存半成品对象了 → 无法解决循环依赖c.如果 A 或 B 不是 Spring 管理的 Bean（比如你自己 new 出来的），Spring 自然没法做循环依赖处理。排查步骤a.确认 A 和 B 上是不是 @Component 或 @Service，并且在 Spring 扫描路径里。b.确认 没加 **@Scope("prototype")**。c.如果用的是 Spring Boot 2.6+，先加上：spring.main.allow-circular-references=trued.检查是否加了 @Transactional 或者别的切面注解。若有，先去掉测试，或者在依赖处加 @Lazy。5.Spring 怎么保证单例 Bean 的创建是线程安全的？DefaultSingletonBeanRegistry 里的缓存机制 + synchronized 锁：DCL机制（学过单例的都知道这个，双重检查锁！），但是Spring这里也是类似于DCL的protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { //类似 DCL 的第一次检查 synchronized(this.singletonObjects) { singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { //再检查三级缓存和二级缓存，类似 DCL 的第二次检查 singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null) { ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }6.Spring是否解决多例Bean的循环依赖详解类型Spring 是否支持循环依赖原因单例（singleton）✅ 支持Spring 可以通过 二级缓存（earlySingletonObjects） 暴露半成品 Bean，从而解决循环依赖多例（prototype）❌ 不支持Spring 不会缓存多例 Bean，每次请求都会创建新的实例，无法提前暴露半成品 Bean为什么多例 Bean 无法解决循环依赖原理：多例 Bean 每次调用 **getBean()** 都会创建新的实例MyBean a = context.getBean(MyBean.class); MyBean b = context.getBean(MyBean.class); // 新实例Spring 不把多例 Bean 放入 singleton 缓存因此没有 earlySingletonObjects 来存半成品 Bean循环依赖时，A 需要 B，但 B 还没创建完成 → Spring 无法提前返回 B 的引用结果：会抛出 BeanCurrentlyInCreationException循环依赖失败Spring 会报错：BeanCurrentlyInCreationException: Error creating bean with name 'A'因为没有缓存机制来提前暴露半成品 Bean@Component @Scope("prototype") class A { @Autowired B b; } @Component @Scope("prototype") class B { @Autowired A a; }解决多例循环依赖的方法1.改成单例 Bean,Spring 可以用三级缓存机制解决循环依赖2.使用 @Lazy 延迟注入3.使用 ObjectFactory 或 Provider 手动注入@Autowired private ObjectProvider bProvider; ... B b = bProvider.getObject();4.重新设计 Bean 依赖关系我觉得这个是最重要的，如果有循环依赖的问题，你要好好想想是不是真的有这个需要，还是说自己的代码设计失误！7.Spring是否解决构造函数中的循环依赖详解a.什么是构造函数循环依赖构造函数循环依赖指的是：两个或多个 Bean 在 构造函数中相互依赖，例如：A 需要 B 构造完成B 需要 A 构造完成形成“死循环”@Component class A { private final B b; public A(B b) { // 构造函数依赖 B this.b = b; } } @Component class B { private final A a; public B(A a) { // 构造函数依赖 A this.a = a; } }b.Spring 是否能解决？✅ 单例 Bean 的 setter/字段注入循环依赖可以解决 ❌ 构造函数循环依赖无法自动解决原因：Spring 的循环依赖解决机制依赖“提前暴露半成品 Bean”二级缓存（**earlySingletonObjects**）存放的是 实例对象三级缓存（**singletonFactories**）存放的是 ObjectFactorySpring 可以在 属性注入阶段暴露半成品 Bean，供其他 Bean 注入构造函数依赖问题构造函数阶段，Bean 必须先创建实例才能被依赖Spring 无法提前创建半成品 Bean（因为构造函数还没执行完成）没有实例可暴露，循环依赖就会导致 BeanCurrentlyInCreationExceptionc.如何解决构造函数循环依赖？改成 setter/字段注入（非构造函数）@Component class A { @Autowired private B b; } @Component class B { @Autowired private A a; }使用 @Lazy 延迟注入8.循环依赖被关闭了?循环依赖被关闭的背景默认情况下：Spring 通过 二级缓存/三级缓存 解决循环依赖（仅限单例 + setter/字段注入）。当我们关闭循环依赖（allowCircularReferences=false）后，Spring 不会把“半成品 Bean”提前暴露出来，因此循环依赖就会直接报错。spring.main.allow-circular-references=falseAOP在循环依赖中会有的问题1.一级缓存 和依赖注入的Bean不一致循环依赖中AOP动态的代理， 初始化就不创建了如果循环以来中创建了初始化时就不会在创建动态代理Bean了小tips：本文可能受个人技术能力限制，如果有错误地方可以指出或者加我微信，评论区自动过滤垃圾评论