SpringBoot-自定义starter
SpringBoot-自定义starter
学习核心
- SpringBoot的starter
- 如何自定义starter
- 实际业务场景项目启动异常排查
学习资料
SpringBoot的starter
starter的引入
使用spring+springmvc框架进行开发的时候如果需要引入mybatis框架,那么需要在xml中定义需要的bean对象,这个过程很明显是很麻烦的,如果需要引入额外的其他组件,那么也需要进行复杂的配置,因此在springboot中引入了starter starter就是一个jar包,写一个@Configuralion的配置类,将这些bean定义在其中,然后再starter包的springboot程序在启动的时候就会按照约定来加载该配置类META-INF/spring.factories
中写入配置类,那么springboot程序在启动的时候就会按照约定来加载该配置类
开发人员只需要将相应的starter包依赖加载到应用中,进行相关的属性配置,就可以进行代码开发,而不需要单独进行bean对象的设置
自定义starter
案例1:自定义模块封装为starter供其他项目使用
构建思路
- 创建一个Demo Project,模拟一个需要被封装的DemoModule模块,提供核心方法为exeModuleMethod
- 通过starter封装可以直接初始化DemoModule的实例到Spring容器
- 在Maven中引入starter,且在yml中配置相应到参数即可直接初始化DemoModule的实例
- 在应用中注入DemoModule即可使用其exeModuleMethod方法
Starter构建
构建步骤
- 创建一个干净的Springboot项目(一般情况下会将实体定义拆分成公共模块,此处为了方便直接和Starter定义在同一个项目),引入starter构建所需依赖
- 定义DemoModule(要暴露的模块定义,包括属性和方法定义,对外提供核心方法)
- 定义DemoProperties(配置文件属性定义,即其他项目引入该依赖时可进行配置的属性,此处的属性和DemoModule中所需要用到的内容有关联)
- 定义DemoAutoConfiguration(此处可以理解为将DemoProperties和DemoModule关联起来,实现自动注入一个DemoModule)
- 在
resources/META-INF/spring.factories
文件中配置EnableAutoConfiguration实现 - 构建完成,则通过
mvn install
指令导入到仓库
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.noob.framework</groupId>
<artifactId>springboot-starter-demoModule</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-starter-demoModule</name>
<description>springboot-starter-demoModule</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.6</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
</build>
</project>
DemoModule(正常情况下可以将DemoModule定义到公共模块,然后再引入,此处为了方便测试统一定义在starter模块)
/**
* 自定义DemoModule
*/
public class DemoModule {
private String version;
private String name;
public String exeModuleMethod(){
return "Demo module, version: " + version + ", name: " + name;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
DemoProperties
// 设定配置前缀
@ConfigurationProperties(prefix = "custom.noob.demo")
public class DemoProperties {
private String version;
private String name;
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
DemoAutoConfiguration
@Configuration
@EnableConfigurationProperties(DemoProperties.class)
public class DemoAutoConfiguration {
@Bean
public DemoModule demoModule(DemoProperties properties){
DemoModule demoModule = new DemoModule();
demoModule.setName(properties.getName());
demoModule.setVersion(properties.getVersion());
return demoModule;
}
}
在
resources/META-INF/spring.factories
文件中配置EnableAutoConfiguration实现
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.noob.framework.config.DemoAutoConfiguration
导出
通过mvn clean install
指定导入到本地仓库,供其他项目引用
Starter使用
使用步骤
- 引入starter依赖:对上上述为springboot-starter-demoModule
- 在application.yml中配置核心参数
- 创建Controller,测试方法调用
pom.xml
<!-- 引入自定义starter -->
<dependency>
<groupId>com.noob.framework</groupId>
<artifactId>springboot-starter-demoModule</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
application.yml
server:
port: 8080
# 配置自定义starter参数
custom:
noob:
demo:
name: haha
version: V1.0.0
Controller构建测试
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
private DemoModule demoModule;
@GetMapping("/execute")
public String execute(){
return demoModule.exeModuleMethod();
}
}
启动项目访问测试
http://localhost:8080/demo/execute
// output:Demo module, version: V1.0.0, name: haha
基于上述案例可以看到,对比传统的构建方式,此处使用者只需要简单通过配置、调用两个步骤即可完成核心方法的调用,而不需要关注其背后的逻辑实现。不同项目中只需要引入starter便能快速接入功能。
如果是传统的构建方式,可能需要对DemoModule的属性进行配置,然后再调用核心方法, 如果在一些复杂的业务场景,需要设置的内容越来越多,就会导致维护起来特别困难,采用配置化的方式更为方便、灵活
案例2:自定义redis-starter
SpringBoot已经提供了redis-starter,此处仅仅是为了强化学习,加深对springboot中的starter的理解
可以从mybatis的起步依赖来进行参考理解。一般第三方的命名规则是功能在前面,spring-boot-starter作为后缀;如果是springboot官方提供则是spring-boot作为前缀,功能最为后缀,以示区分
实现步骤
- 创建redis-spring-boot-autoconfigure模块,并在该模块中初始化Jedis的Bean,并配置META-INF/spring.factories文件
- 创建redis-spring-boot-starter模块,依赖redis-spring-boot-autoconfigure模块
- 创建测试模块,引入自定义的redis-starter依赖,测试获取Jedis的Bean,操作redis
优化:条件加载(只有引入Jedis类才加载配置;如果用户已经定义了Jedis则不加载),然后去参考springboot 提供的自动配置参考
redis-spring-boot-autoconfigure
redis-spring-boot-autoconfigure构建核心:
- 提供RedisProperties(配置实体类@ConfigurationProperties,将实体属性和配置文件绑定)
- 提供RedisAutoConfiguration(自动配置:@Configuration、启用配置文件@EnableConfigurationProperties(RedisProperties.class))
- 在resources/WEB-INF/spring.factories中配置EnableAutoConfiguration
pom.xml
构建一个干净的springboot项目,去除无关的依赖和build内容,引入要使用到的依赖(jedis)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.noob.framework</groupId>
<artifactId>redis-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-spring-boot-autoconfigure</name>
<description>redis-spring-boot-autoconfigure</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.6</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 引入jedis依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
</build>
</project>
RedisProperties
@ConfigurationProperties(prefix = "custom.redis")
注解的作用是将实体类与配置文件定义进行绑定,当用户在yml(properties)配置指定前缀的内容,则可相应绑定到实体中
@ConfigurationProperties(prefix = "custom.redis")
public class RedisProperties {
private String host;
private Integer port;
private String password;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
当配置完成,需要借助其他注解将其注册到Spring容器中,让其被Spring识别。此处选择在RedisAutoConfiguration中去装配这个内容
RedisAutoConfiguration
@Configuration
:指定当前类为一个配置类
@EnableConfigurationProperties(RedisProperties.class)
:当Springboot项目启动的时候会自动创建该Bean对象放到SpringIOC容器中
// 配置类
@Configuration
@EnableConfigurationProperties(RedisProperties.class)// 将RedisProperties在启动时自动注册到SpringIOC容器中
public class RedisAutoConfiguration {
@Bean
// 提供Jedis的Bean(将RedisProperties通过参数的方式注入进来,即可动态根据配置生成Jedis链接)
public Jedis jedis(RedisProperties redisProperties){
// return new Jedis("localhost",6379);
// 如果远程服务器设定了auth,则必须指定密码,否则链接错误
Jedis jedis = new Jedis(redisProperties.getHost(),redisProperties.getPort());
String password = redisProperties.getPassword();
if(!StringUtils.isEmpty(password)){
jedis.auth(redisProperties.getPassword());
}
return jedis;
}
}
resources/WEB-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.noob.framework.config.RedisAutoConfiguration
打包
构建完成,将项目导入本地仓库
redis-spring-boot-starter(起步依赖)
构建一个在redis-spring-boot-starter项目中引入redis-spring-boot-autoconfigure。这个项目中不需要做任何配置,只需要引入autoconfigure
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.noob.framework</groupId>
<artifactId>redis-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-spring-boot-starter</name>
<description>redis-spring-boot-starter</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.6</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 引入自定义的autoconfigure -->
<dependency>
<groupId>com.noob.framework</groupId>
<artifactId>redis-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
</build>
</project>
打包
通过mvn clean install
指令将项目导入本地仓库
spring-boot-starter-test
构建正常的springboot项目,在项目中引入redis-spring-boot-starter,随后编写代码验证
构建步骤
- 在application.yml中配置参数
- 编写接口验证自定义的redis-starter或者在启动类中直接测试
application.yml
server:
port: 8080
# 配置自定义starter参数
custom:
redis:
host: localhost
port: 6379
password: 123456
对于基础的Jedis连接只需要提供host、port,如果redis设置了auth则不需指定password,否则链接错误
测试1:接口访问测试
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
接口访问测试:http://localhost:8080/demo/link,正常响应会返回响应的字符串数据
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
private Jedis jedis;
@GetMapping("/link")
public String link(){
// 测试获取redis
System.out.println(jedis);
// 测试jedis连接配置
jedis.set("hello","hello starter");
return "success:" + jedis.get("hello");
}
}
// output
BinaryJedis{Connection{DefaultJedisSocketFactory{localhost:6379}}}
测试2:直接在启动类中进行配置
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
// ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
// Jedis jedis = context.getBean(Jedis.class);
// System.out.println(jedis);
}
}
此处需注意对象构建成功并不等同于连接成功,还需要调用访问才能确认是否可以连接访问
starter优化
redis-spring-boot-autoconfigure:对自动配置加限定条件,例如如果没有找到Jedis则不装配,或者是如果已经定义了同名的对象则不装配(这点对照可以参考Springboot自动装配原理去理解,可以参考Springboot提供的RedisAutoConfiguration实现),例如此处自定义RedisAutoConfiguration优化如下
// 配置类
@Configuration
@EnableConfigurationProperties(RedisProperties.class)// 将RedisProperties在启动时自动注册到SpringIOC容器中
@ConditionalOnClass(Jedis.class)// 限定条件:只有引入Jedis依赖(加载了Jedis字节码文件)才装配配置
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = {"jedis"})
// 提供Jedis的Bean(将RedisProperties通过参数的方式注入进来,即可动态根据配置生成Jedis链接)
public Jedis jedis(RedisProperties redisProperties){
System.out.println("RedisAutoConfiguration 装配,注册Jedis对象");
// return new Jedis("localhost",6379);
// 如果远程服务器设定了auth,则必须指定密码,否则链接错误
Jedis jedis = new Jedis(redisProperties.getHost(),redisProperties.getPort());
String password = redisProperties.getPassword();
if(!StringUtils.isEmpty(password)){
jedis.auth(redisProperties.getPassword());
}
return jedis;
}
}
springboot-starter-test中测试:如果自定义了名为“jedis”的Bean对象,则会使用test模块中自定义的Bean,而不会去装配autoconfigure模块默认提供的Bean。
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
// SpringApplication.run(DemoApplication.class, args);
ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
Jedis jedis = context.getBean(Jedis.class);
System.out.println(jedis);
}
@Bean
public Jedis jedis() {
System.out.println("自定义的Jedis对象");
Jedis jedis = new Jedis("127.0.0.1", 6379);
return jedis;
}
}
// output
自定义的Jedis对象
2024-06-13 16:34:26.688 INFO 8521 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2024-06-13 16:34:26.703 INFO 8521 --- [main] com.noob.framework.DemoApplication : Started DemoApplication in 1.904 seconds (JVM running for 2.494)
BinaryJedis{Connection{DefaultJedisSocketFactory{127.0.0.1:6379}}}
常见问题
如果在测试模块中启动出现如下错误,说明Jedis没有初始化成功,可能是因为redis-spring-boot-autoconfigure配置错误导致,需检查对应工程的注解配置和内容。
案例参考
mybatis-spring-boot-starter
以Springboot提供的mybatis起步依赖进行分析,拆解自定义starter的实现,理解Springboot是如何通过配置化整合MyBatis框架。在任意Springboot项目中引入mybatis-spring-boot-starter
(理解Springboot提供的mybatis起步依赖,就能更好理解上述自定义redis起步依赖的构建思路)
<!-- 引入mybatis-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
刷新maven项目,随后查看关联依赖,关注到mybatis-spring-boot-autoconfigure
、mybatis-spring-boot-starter
mybatis-spring-boot-starter
查看其实现
可以看到mybatis-spring-boot-starter
这个工程非常“干净”,其核心在于pom.xml引入了mybatis-spring-boot-autoconfigure
模块
进一步跟踪
mybatis-spring-boot-autoconfigure
模块
mybatis-spring-boot-autoconfigure
是一个普通的Springboot工程,其主要用于构建自动配置(将xml配置和实体进行绑定,并提供一些核心方法供外部调用),相应地,定位MybatisProperties、MybatisAutoConfiguration的方法实现,看其是如何实现配置绑定的
MybatisProperties
的定义用于将application.yml中的配置定义和对应的实体进行绑定
MybatisAutoConfiguration
的定义用于加载MybatisProperties
到SpringIOC容器,并通过获取到的配置信息去装配相应的Bean对象或者执行相关操作
业务场景启动失败排查案例
理解了上述自动装配的流程,在Springboot日常项目开发中可能经常会遇到一些项目报错的信息,则可关注在启动的时候是否因为一些Bean初始化错误导致的启动失败,定位到具体的核心代码实现然后分析原因。
这种场景可以进一步扩展到业务模块开发,尤其是一些公司自研的项目,自定义封装组件,经常会引入一些自定义的构件,在初始化自动装配某些内容,如果项目是协作开发,在同步代码之后就会出现各种莫名其妙的启动失败的问题,因为开发者可能并不知道其他成员在这个项目中更新了什么内容导致项目启动失败,则需要通过排查异常堆栈信息来进一步跟踪定位。
举个简单的业务案例场景:基于上述的案例1进行说明,假设在装配某个核心模块的时候一些参数是必不可少的,因此需要限定相应的校验(可能这个过程中直接简单粗暴抛出业务异常提示等),但实际上直接引用对应起步依赖的开发者来说只能通过启动日志去跟踪这种启动异常的情况。(需注意此处案例1直接构建了一个starter,没有单独拆出起步依赖概念)
以DemoModule为例,提供一个构造方法,限定在初始化DemoModule对象的时候必须指定name属性,否则抛出异常提示
修改DemoModule类,新增构造函数
public DemoModule() {}
public DemoModule(String name,String version) {
// 校验参数信息
if(StringUtils.isEmpty(name)){
throw new IllegalArgumentException("name is empty");
}
if(StringUtils.isEmpty(version)){
throw new IllegalArgumentException("version is empty");
}
this.version = version;
this.name = name;
}
修改DemoAutoConfiguration,新增一个根据带参构造函数初始化的Bean对象创建方法
@Bean(name = "demoModuleByParam")
public DemoModule demoModuleByParam(DemoProperties properties){
System.out.println("demoModuleByParam执行 初始化DemoModule");
DemoModule demoModule = new DemoModule(properties.getName(), properties.getVersion());
return demoModule;
}
更新依赖
执行mvn clean install
更新本地仓库依赖
测试:springboot-starter-test模块
@RestController
@RequestMapping("/demoModule")
public class DemoModuleController {
@Resource(name = "demoModuleByParam")
private DemoModule demoModule;
@GetMapping("/execute")
public String execute(){
return demoModule.exeModuleMethod();
}
}
校验异常情况,故意设定不传name属性
# 配置自定义starter参数
custom:
noob:
demo:
name:
version: V1.0.0
根据启动异常信息分析问题(不要带着答案找问题,仅仅根据堆栈信息去定位问题)
启动Springboot工程,会发现项目尝试自动装配DemoModule对象,但是执行失败,进一步跟踪项目日志(定位后面的日志信息)
如果开发者并不了解这个机制,就会莫名其妙被这个堆栈信息给绕晕,需要一步步向上定位日志的堆栈信息,确认异常抛出具体问题。
因此需要进一步跟踪这个Bean创建时是什么情况下触发了异常,进入查看相应的方法实现,可以根据关键字定位到具体异常抛出的位置,是因为name属性为空抛出异常。进一步跟踪这个name属性来源,这个name属性则是和工程中yml配置绑定的
结合上述思路,可以看到是因为在test模块中引入了demoModule模块,但是在指定配置的时候并没有设置name属性,进而触发异常。也就印证了一开始没有配置name属性就是为了触发这种异常。完整的堆栈信息参考:
2024-06-13 17:59:12.614 INFO 13328 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1047 ms
demoModuleByParam执行 初始化DemoModule
2024-06-13 17:59:12.723 WARN 13328 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'demoModuleController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'demoModuleByParam' defined in class path resource [com/noob/framework/config/DemoAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.noob.framework.module.DemoModule]: Factory method 'demoModuleByParam' threw exception; nested exception is java.lang.IllegalArgumentException: name is empty
2024-06-13 17:59:12.728 INFO 13328 --- [ main] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2024-06-13 17:59:12.748 INFO 13328 --- [ main] ConditionEvaluationReportLoggingListener :
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2024-06-13 17:59:12.775 ERROR 13328 --- [ main] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'demoModuleController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'demoModuleByParam' defined in class path resource [com/noob/framework/config/DemoAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.noob.framework.module.DemoModule]: Factory method 'demoModuleByParam' threw exception; nested exception is java.lang.IllegalArgumentException: name is empty
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessProperties(CommonAnnotationBeanPostProcessor.java:332) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) [spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) [spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) [spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) [spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) [spring-boot-2.7.6.jar:2.7.6]
at com.noob.framework.DemoApplication.main(DemoApplication.java:10) [classes/:na]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'demoModuleByParam' defined in class path resource [com/noob/framework/config/DemoAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.noob.framework.module.DemoModule]: Factory method 'demoModuleByParam' threw exception; nested exception is java.lang.IllegalArgumentException: name is empty
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:658) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:638) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.resolveBeanByName(AbstractAutowireCapableBeanFactory.java:479) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.autowireResource(CommonAnnotationBeanPostProcessor.java:550) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.getResource(CommonAnnotationBeanPostProcessor.java:520) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor$ResourceElement.getResourceToInject(CommonAnnotationBeanPostProcessor.java:673) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.annotation.InjectionMetadata$InjectedElement.inject(InjectionMetadata.java:228) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessProperties(CommonAnnotationBeanPostProcessor.java:329) ~[spring-context-5.3.24.jar:5.3.24]
... 17 common frames omitted
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.noob.framework.module.DemoModule]: Factory method 'demoModuleByParam' threw exception; nested exception is java.lang.IllegalArgumentException: name is empty
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) ~[spring-beans-5.3.24.jar:5.3.24]
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653) ~[spring-beans-5.3.24.jar:5.3.24]
... 33 common frames omitted
Caused by: java.lang.IllegalArgumentException: name is empty
at com.noob.framework.module.DemoModule.<init>(DemoModule.java:24) ~[springboot-starter-demoModule-0.0.1-SNAPSHOT.jar:na]
at com.noob.framework.config.DemoAutoConfiguration.demoModuleByParam(DemoAutoConfiguration.java:23) ~[springboot-starter-demoModule-0.0.1-SNAPSHOT.jar:na]
at com.noob.framework.config.DemoAutoConfiguration$$EnhancerBySpringCGLIB$$41fcb83.CGLIB$demoModuleByParam$1(<generated>) ~[springboot-starter-demoModule-0.0.1-SNAPSHOT.jar:na]
at com.noob.framework.config.DemoAutoConfiguration$$EnhancerBySpringCGLIB$$41fcb83$$FastClassBySpringCGLIB$$6305e43f.invoke(<generated>) ~[springboot-starter-demoModule-0.0.1-SNAPSHOT.jar:na]
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) ~[spring-core-5.3.24.jar:5.3.24]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) ~[spring-context-5.3.24.jar:5.3.24]
at com.noob.framework.config.DemoAutoConfiguration$$EnhancerBySpringCGLIB$$41fcb83.demoModuleByParam(<generated>) ~[springboot-starter-demoModule-0.0.1-SNAPSHOT.jar:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_412]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_412]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_412]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_412]
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.3.24.jar:5.3.24]
... 34 common frames omitted
上述这个问题场景是出现在多人协作开发的时候,有成员更新了代码版本,而这个版本设定了一些自定义的启动配置,如果说其他成员不了解这个情况或者机制的话,直接启动项目就会报错(原因可能在于缺少一些核心配置或者其他环境问题导致),这种情况也会经常出现在在引入一些第三方jar的时候,如果没有按照限定规则正确配置的话就会出现各种莫名其妙的异常(例如构建数据库连接的时候引入了jdbc相关jar,但是却没有配置jdbc参数,就会触发异常),也就是我们经常说到的一开始还好好的,加了个依赖或者更新了代码,突然间就不能跑了,不排除是各种各样的因素导致,尤其是引入第三方jar,需要结合异常的堆栈信息一步步进行跟踪
一般项目异常排查跟踪思路参考
- 定位异常信息,关键字检索
- 定位最后位置的异常提示,确认抛出的异常信息,在项目中进行关键字检索跟踪
- 针对核心关键提示(关键字段),定位其对应的方法实现
- 关键字检索,用于排查是项目自身抛出的异常还是引入了第三方jar触发异常
- 如果是自身项目抛出异常,则进一步定位代码实现一步步断掉调试排查即可(全局检索定位异常抛出位置,然后分析可能出现异常的情况)
- 如果是引入第三方jar(例如上述案例中引入自定义demoModule模块),则需要根据核心关键字查看源码实现进行跟踪(这种往往比较难排查,无法全局检索,需要一步步通过断点调试跟踪代码实现)
实际业务场景分析
以实际业务场景为例,剖析项目启动异常问题排查的思路。目前有一个项目是基于apollo配置中心搭建的微服务项目,会有不同的开发组成员对这个项目进行改造维护以适配业务场景,在这个过程中不免要处理可能存在的一些代码冲突、配置不同步等问题。引入apollo配置中心是为了适配不同的集群环境的项目配置,此处可以先简单对比一下传统Springboot项目的多环境配置
传统的单体Springboot项目的多环境配置,是通过构建多套不同的application-xxxx.yml文件来实现,只需要在对应的主配置文件application.yml中指定激活的配置文件就能够切换到对应的配置。针对生产环境的“容器外挂配置文件”概念也是多环境配置的一种体现,即开发者不会将生产的配置文件放到项目中维护,而是通过外挂的形式,在项目启动的时候指定要激活的外部配置文件来进行设定。
但随着业务场景的扩展,后续可能需要维护的参数配置越来越多,需要设置和维护的参数配置也要相应同步就会使得配置文件维护会变得困难,有时候甚至因为不同模块开发的配置切换导致各种冲突、异常情况。
为了解决上述多环境配置的场景,引入apollo配置中心,通过构建不同的配置,在项目中只需要通过类似唯一键的字符串来关联对应环境的配置信息,开发者可以方便地在线修改配置信息。(此外还有配置同步、历史跟踪等功能,可以定位配置修改记录)。
那么此处就会存在一个问题,就是旧项目改造配置的切换和多人协作开发的配置同步问题,可能一些以往的老项目是沿用本地配置的概念,后面迁移到apollo统一线上配置化,刚接手项目的时候会有些概念混淆,为什么有些参数可以在本地配置文件跟踪到、有些配置却没有(在apollo配置中心中设置了),所以要理解这两种不同的配置化方式的应用才能更好地排查问题。
针对多人协作开发的配置同步问题,则是因为每个成员负责的模块不同,正常情况下如果是接口交互一般情况下不太会影响项目启动,且一般有代码提交规范和review的场景,不会随意将异常代码提交上去。那么就是存在一种可能:有成员在项目中引入了一些自定义内容在其自己的开发环境中是可以正常运行的,但是其他成员同步后由于配置环境没有同步就会导致异常。常见的异常情况可能有如下:
- 代码同步后、配置未同步:例如项目中引入了一些启动时自动加载的内容或者引入第三方依赖,但是不同开发环境的配置并没有同步,从而导致项目启动异常,其排查思路可以参考上述案例去分析
- 配置先行、代码未同步:在对应开发环境中先装配了配置,但是可能代码还没同步(开发到一半)。这种情况则要考虑实际的代码同步场景,不要随意合并分支,这种操作非常危险,只能去和小组成员沟通确认是哪块代码未同步导致启动异常。初定解决方案就是切换到其他正常环境(先取消掉异常的配置信息,切换到正常的环境)去尝试
注意apollo参数配置的驼峰转化:例如custom.param.dev-server,可能对应到实体为前缀(custom.param) + devServer
继续往下分析,如果项目启动出现了上述的异常情况,那么对照最后定位到是参数配置问题,但是这个配置并没有在本地项目显式定义,而是通过配置中心加载获取的,因此要进入到apollo配置中心查看对应环境的集群配置,找到相应的配置文件,确认是否存在该参数配置。如果没有,则需要进行配置(一般是从现有稳定环境版本中进行配置同步,进而避免一些参数胡乱配置触发的异常)。而在日常开发中一些代码版本更新后,但是对应开发环境配置并没有同步(因为在开发过程中这个节点是会有时间差的,并不是所有的改动都会明确通知到位,一些开发过程中的信息差就会导致各种环境配置问题:有可能是配置先行、代码还没更新导致启动异常;有可能是代码更新、而配置未同步导致启动异常),因此更要了解相应的项目架构、技术栈原理,掌握核心的异常排查技巧,一步步定位排查问题,学会跟踪堆栈信息。