spring+mybatis启动NoClassDefFoundError异常分析三部曲之二:定位错误

简介: spring+mybatis项目启动失败,定位和跟踪问题

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码): https://github.com/zq2599/blog_demos
  • spring+mybatis项目启动失败,报错:
java.lang.NoClassDefFoundError: Could not initialize class org.springframework.beans.factory.BeanCreationException

重现问题

  • 继续打开上一章的工程,源码在Git上,地址:git@github.com:zq2599/blog_demos.git,下载后可以发现里面有很多工程,本次实战用的工程是springmybatisexceptiondemo,如下图红框所示:

这里写图片描述

  • 打开工程,首先确保日志已经改为debug级别了,如下图:

这里写图片描述

  • 再打开spring-mybatis.xml文件,确保org.mybatis.spring.mapper.MapperScannerConfigurer的配置中,没有配置sqlSessionFactoryBeanName,如下图,sqlSessionFactoryBeanName配置是注释掉的:

这里写图片描述

  • ok,打包,部署吧,可以看到如下错误信息:
java.lang.NoClassDefFoundError: Could not initialize class org.springframework.beans.factory.BeanCreationException
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:547)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:700)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482)
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:403)
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:306)
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:106)
    ...
  • tomcat的堆栈信息太占篇幅我就不贴出来了,只留了spring的;

第一个前提条件:spring配置会出发启动失败

  • 在根据堆栈信息去打断点调试之前,我们先把MapperScannerConfigurer这个bean的配置信息搞清楚,sqlSessionFactoryBeanName这个属性对MapperScannerConfigurer的实例有什么影响呢?
  • 打开MapperScannerConfigurer.java,在postProcessBeanDefinitionRegistry方法中看到sqlSessionFactoryBeanName属性被设置给ClassPathMapperScanner对象了,如下图:

这里写图片描述

  • 看ClassPathMapperScanner的doScan方法,这个方法是对basePackages路径下所有接口的动态代理对象设置属性,如果ClassPathMapperScanner对象中sqlSessionFactoryBeanName为空,就设置这些动态代理对象的autowire属性为AUTOWIRE_BY_TYPE,如下图:

这里写图片描述

  • 也就是说,这次工程启动异常的原因,就是mybatis的mapper接口的动态代理对象需要sqlSessionFactory对象,此对象的获取有两种情况:
  1. 这个对象如果可以从MapperScannerConfigurer的sqlSessionFactoryBeanName属性直接指定,这项目就能启动成功;
  2. 如果没有在xml中为MapperScannerConfigurer指定sqlSessionFactoryBeanName属性,就会走另一个逻辑,在生成动态代理对象时,由spring环境寻找合适类型的bean去注入,此时项目会启动失败;

第二个前提条件:动态代理类数量增加会导致启动失败

  • 大家看一下demo源码com.ssm.dao包下面,有很多个Mapper接口,这里的每一个接口,在spring启动的时候mybatis环境都会生成一个动态代理对象,当接口只有少量的时候,即便没有配置MapperScannerConfigurer属性,工程也能启动成功,数量逐渐增加到一定程度(具体数量和接口的复杂程度以及栈大小有关),就会启动失败,如下图:

这里写图片描述

断点追踪

  • 准备工作都ok了,咱们来通过打断点单步执行的方法来定位问题的位置吧,我用的是Intellij idea,此工具远程debug连接tomcat的的具体步骤请参照文章《Intellij idea远程debug连接tomcat,实现单步调试》
  • 根据前面的堆栈日志,我们在ContextLoaderListener.java的106行打下断点,启动tomcat之后线程就会在此暂停,如下图:

这里写图片描述

  • 然后就是进入方法内部单步执行了,在关键位置打了个断点,如下图:

这里写图片描述

  • 信息很丰富,从左下角绿框中的堆栈可以看出一个bean的初始化调用层次:
  • 在实例化userController的时候要处理它的userService属性,所以走到了CommonAnnotationBeanPostProcessor.autowireResource方法内,通过factory.getBean来获取userService对象,并且传入了name和type。
  • 由于userService还没有创建,因此此处立即开始创建userService实例,很快,又执行到了CommonAnnotationBeanPostProcessor.autowireResource方法内,如下图:

这里写图片描述

  • 创建userService的时候,需要注入userDao属性,于是又会触发userDao实例的创建,但是,这次不是通过factory.getBean来创建了,而是DefaultListableBeanFactory.resolveDependency。
  • 为什么userService对象是通过factory.getBean创建,而userDao却是通过DefaultListableBeanFactory.resolveDependency来创建的呢?
  • 我们去看下工程中UserService.java,如下图红框所示,UserService已经通过注解声明为service组件了,所以在CommonAnnotationBeanPostProcessor.autowireResource方法中,factory.containsBean("userService")会返回true,而userDao呢?整个工程中没有一个类通过@Service标签声明为userDao组件,所以只能通过DefaultListableBeanFactory.resolveDependency去找了,传入了一堆信息給resolveDependency方法,如下图:

这里写图片描述

  • resolveDependency方法会调用doResolveDependency方法,对于要注入的属性,例如userService对象的userDao属性,由于要根据属性类型来注入,所以要先找出一些候选人,在这些候选人中筛选出匹配的bean来完成注入,此逻辑对应的代码如下图:

这里写图片描述

  • 在findAutowireCandidates中有几步比较重要:
  1. 要根据所需的对象类型查找beanName,在doGetBeanNamesForType方法中,通过getBeanDefinitionNames拿到了所有的bean的名称;
  2. 拿到这些名称后,对每个名称都调用isTypeMatch方法,传入名称和属性的类型;
  3. isTypeMatch方法的目的是根据bean名称找到bean实例,再看这个实例的类型和传入的类型是否一致;
  4. isTypeMatch方法中需要根据bean名称找到bean实例,因此,对于没有实例化过的bean名称,就会触发bean的实例化,最终走到了AbstractBeanFactory.doGetBean方法,这里面自定义了一个ObjFactory,里面执行了createBean方法,如下图:

这里写图片描述

  • 现在,我们在上图中的createBean方法处打断点,然后将代码执行下去,可以发现以下循环的逻辑:
  1. createBean的时候,由于要处理这个bean的依赖属性(例如user002Mapper的SqlSessionFactory属性),于是会调用DefaultListableBeanFactory.doResolveDependency方法;
  2. doResolveDependency方法中,要执行findAutowireCandidates方法获取所有的候选人,然后找出符合要求的bean赋給依赖属性(例如user002Mapper的SqlSessionFactory属性);
  3. findAutowireCandidates中的步骤我们刚才已经分析过了,对于没有实例化过的bean名称,就会触发bean的实例化,最终又走到了AbstractBeanFactory.doGetBean方法;
  4. 又回到了步骤一,只不过这次createBean创建的是另一个bean了;
  5. 在createBean处的断点不停的继续执行,最终在创建userXXXMapper的时候发生了StackOverflowError,我的本地电脑是user019Mapper;
  • 结合我们的工程可以这么解释了:
  1. createBean打算创建userService;
  2. userService有个属性需要注入,这个属性的类型是UserMapper;
  3. 寻找属性为UserMapper的bean时候,执行findAutowireCandidates方法去查找合适的属性;
  4. 查找过程是按照所有单例的bean的名称,根据bean的名称挨个查的,找到了user001Mapper这个beanname;
  5. user001Mapper的实例并不存在,于是执行createBean创建user001Mapper;
  6. user001Mapper有个属性需要注入,这个属性的类型是SqlSessionFactory;
  7. 寻找属性为SqlSessionFactory的bean时候,执行findAutowireCandidates方法去查找合适的属性;
  8. 查找过程是根据beanname挨个查的,找到了user002Mapper这个beanname;
  9. user002Mapper的实例并不存在,于是执行createBean创建user002Mapper;
  10. user002Mapper有个属性需要注入,这个属性的类型是SqlSessionFactory;
  11. ......
  12. 执行createBean创建user003Mapper......
  13. ......
  14. 执行createBean创建user004Mapper......
  15. ......
  16. 每一次createBean都是在上一次createBean执行的过程中被调用的,堆栈层次会越来越深;
  17. com.ssm.dao包下面的接口越多,对应的动态代理实例就越多,此处的堆栈就越深;
  18. 深到一定层次的时候,例如创建user019Mapper时,就会抛出StackOverflowError异常了;
  19. 在AbstractAutowireCapableBeanFactory.doCreateBean方法中,对创建bean时抛出的异常做了try...catch处理,捕获到StackOverflowError之后,抛出的是beanName参数为user019Mapper的BeanCreationException,如下图:

这里写图片描述

  1. 按照方法堆栈层次的关系,创建user019Mapper时抛出BeanCreationException异常后,回到了创建user018Mapper的doCreateBean方法中,此时捕获的异常又被包装成beanName参数为user018Mapper的BeanCreationException;
  2. 按照上述的捕获抛出逻辑一层一层返回堆栈,最终抛出的异常是beanName参数为userController的BeanCreationException;
  • 至此真相大白,在spring依赖注入的时候,AUTOWIRE_BY_TYPE类型的注入,总是要挨个获取所有bean的类型,从中选出类型合适的bean来注入,获取这些bean的过程中,如果还没有实例化就立即做实例化,做的时候又要对这些bean自身的属性进行注入,于是就出现了AbstractAutowireCapableBeanFactory.createBean方法的一层一层嵌套式调用,bean越多嵌套越深,导致栈内存被耗光

重要推断

  • 根据以上的分析和追踪,我们可以推断出一种临时避免启动失败的方法,就是把栈的大小在java启动参数中配置得大一些,但这种方法是不可靠的,因为随着动态代理类数量的增加,栈的消耗越来越大,很有可能会再次耗尽栈内存,所以配置MapperScannerConfigurer的sqlSessionFactoryBeanName属性,以此来避免AUTOWIRE_BY_TYPE带来的栈层次加深才是可靠办法。
  • 以上就是定位和分析异常的过程,看懂了整个过程,再回头来看看spring启动时抛出的异常,如下图,很多关键信息都被没有输出,如果不打断点,仅凭输出信息来定位问题是很难定位到问题所在的,下一篇,三部曲之三,我们去修改和编译spring的源码,让spring环境在抛出异常时带上更详细的错误信息。

这里写图片描述

  • 对修改spring源码有兴趣的读者,可以先看下这篇文章《修改和编译spring源码,构建jar(spring-context-4.0.2.RELEASE)》,然后,咱们在下一章《spring+mybatis启动NoClassDefFoundError异常分析三部曲之三:改spring源码,取详细错误》一起动手实践;

欢迎关注阿里云开发者社区博客:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...
相关文章
|
8月前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于 xml 的整合
本教程介绍了基于XML的MyBatis整合方式。首先在`application.yml`中配置XML路径,如`classpath:mapper/*.xml`,然后创建`UserMapper.xml`文件定义SQL映射,包括`resultMap`和查询语句。通过设置`namespace`关联Mapper接口,实现如`getUserByName`的方法。Controller层调用Service完成测试,访问`/getUserByName/{name}`即可返回用户信息。为简化Mapper扫描,推荐在Spring Boot启动类用`@MapperScan`注解指定包路径避免逐个添加`@Mapper`
424 0
|
5月前
|
Java 数据库连接 数据库
Spring boot 使用mybatis generator 自动生成代码插件
本文介绍了在Spring Boot项目中使用MyBatis Generator插件自动生成代码的详细步骤。首先创建一个新的Spring Boot项目,接着引入MyBatis Generator插件并配置`pom.xml`文件。然后删除默认的`application.properties`文件,创建`application.yml`进行相关配置,如设置Mapper路径和实体类包名。重点在于配置`generatorConfig.xml`文件,包括数据库驱动、连接信息、生成模型、映射文件及DAO的包名和位置。最后通过IDE配置运行插件生成代码,并在主类添加`@MapperScan`注解完成整合
936 1
Spring boot 使用mybatis generator 自动生成代码插件
|
5月前
|
Java 数据库连接 API
Java 对象模型现代化实践 基于 Spring Boot 与 MyBatis Plus 的实现方案深度解析
本文介绍了基于Spring Boot与MyBatis-Plus的Java对象模型现代化实践方案。采用Spring Boot 3.1.2作为基础框架,结合MyBatis-Plus 3.5.3.1进行数据访问层实现,使用Lombok简化PO对象,MapStruct处理对象转换。文章详细讲解了数据库设计、PO对象实现、DAO层构建、业务逻辑封装以及DTO/VO转换等核心环节,提供了一个完整的现代化Java对象模型实现案例。通过分层设计和对象转换,实现了业务逻辑与数据访问的解耦,提高了代码的可维护性和扩展性。
218 1
|
4月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
150 0
|
5月前
|
SQL Java 数据库
解决Java Spring Boot应用中MyBatis-Plus查询问题的策略。
保持技能更新是侦探的重要素质。定期回顾最佳实践和新技术。比如,定期查看MyBatis-Plus的更新和社区的最佳做法,这样才能不断提升查询效率和性能。
223 1
|
5月前
|
负载均衡 Java API
基于 Spring Cloud 的微服务架构分析
Spring Cloud 是一个基于 Spring Boot 的微服务框架,提供全套分布式系统解决方案。它整合了 Netflix、Zookeeper 等成熟技术,通过简化配置和开发流程,支持服务发现(Eureka)、负载均衡(Ribbon)、断路器(Hystrix)、API网关(Zuul)、配置管理(Config)等功能。此外,Spring Cloud 还兼容 Nacos、Consul、Etcd 等注册中心,满足不同场景需求。其核心组件如 Feign 和 Stream,进一步增强了服务调用与消息处理能力,为开发者提供了一站式微服务开发工具包。
593 0
|
7月前
|
SQL 前端开发 Java
深入分析 Spring Boot 项目开发中的常见问题与解决方案
本文深入分析了Spring Boot项目开发中的常见问题与解决方案,涵盖视图路径冲突(Circular View Path)、ECharts图表数据异常及SQL唯一约束冲突等典型场景。通过实际案例剖析问题成因,并提供具体解决方法,如优化视图解析器配置、改进数据查询逻辑以及合理使用外键约束。同时复习了Spring MVC视图解析原理与数据库完整性知识,强调细节处理和数据验证的重要性,为开发者提供实用参考。
318 0
|
8月前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于注解的整合
本文介绍了Spring Boot集成MyBatis的两种方式:基于XML和注解的形式。重点讲解了注解方式,包括@Select、@Insert、@Update、@Delete等常用注解的使用方法,以及多参数时@Param注解的应用。同时,针对字段映射不一致的问题,提供了@Results和@ResultMap的解决方案。文章还提到实际项目中常结合XML与注解的优点,灵活使用两者以提高开发效率,并附带课程源码供下载学习。
667 0
|
10月前
|
前端开发 Java 数据库连接
Java后端开发-使用springboot进行Mybatis连接数据库步骤
本文介绍了使用Java和IDEA进行数据库操作的详细步骤,涵盖从数据库准备到测试类编写及运行的全过程。主要内容包括: 1. **数据库准备**:创建数据库和表。 2. **查询数据库**:验证数据库是否可用。 3. **IDEA代码配置**:构建实体类并配置数据库连接。 4. **测试类编写**:编写并运行测试类以确保一切正常。
441 2
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
578 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。