Spring Core常见错误及解决方案

Spring Core常见错误及解决方案

一些Spring Core错误及解决方案,出自极客时间傅健老师《Spring编程常见错误50例》

https://time.geekbang.org/column/intro/100077001

Bean定义

隐式扫描不到Bean的定义

image-20240821091058169

如果我们定义这样的目录结构,实际上访问对应接口时会找不到。

原因是@SpringBootApplication注解里的@ComponentScan注解中的basePackages属性指定了应用启动时扫描的Bean目录,如果不显式指定,默认扫描主启动类下的包,因此扫描不到HelloWorldController了,导致Bean失效。

解决方案

通过@ComponentScans/@ComponentScan添加需要扫描的包路径,注意如果添加这个注解后主启动类所在包就不会再被扫描!

定义的 Bean 缺少隐式依赖

image-20240821091848633

如果我们的Bean中有某个属性值,Bean在启动时需要通过构造器构造,但是找不到这个属性的Bean,启动有时候就会报错:Parameter 0 of constructor in * required a bean of type 'java.lang.String' that could not be found.

解决方案

image-20240821091931742

定义一个这样的Bean,供构造器使用

注意:显式定义构造器,会自动发生根据构造器参数寻找对应bean的过程。可以给参数添加@Autowired((required = false)注解。

原型 Bean 被固定

image-20240821092233169

image-20240821092152064

当一个属性成员 serviceImpl 声明为 @Autowired 后,那么在创建HelloWorldController这个 Bean 时,会先使用构造器反射出实例,然后来装配各个标记为 @Autowired 的属性成员,这个装配的执行只发生了一次,所以后续就固定起来了,它并不会因为 ServiceImpl 标记了 SCOPE_PROTOTYPE 而改变。

解决方案

image-20240821092433145

每次从Context中取。

依赖注入

过多赠予,无所适从

错误为:required a single bean, but * were found

直接翻译为需要一个bean,但是却提供了多个

public interface DataService {
	void deleteStudent(int id);
}

@Repository
@Slf4j
public class OracleDataService implements DataService{
    @Override
    public void deleteStudent(int id) {
        log.info("delete student info maintained by oracle");
    }
}

@Repository
@Slf4j
public class MysqlDataService implements DataService{
    @Override
    public void deleteStudent(int id) {
        log.info("delete student info maintained by mysql");
    }
}

这时我们在使用下面这个代码运行时就会发生错误:

@Autowired
private DataService dataService;

当程序在装配dataService时候,发现有MysqlDataService和OracleDataService可以选择,并且决策不出优先级(@Primary注解),因此无法正常运行。

解决方案

  1. 给其中一个Bean加上@Primary注解,让其优先级更高
  2. 不要使用private DataService dataService,而是将命名改为bean的名字private DataService oracleDataService
  3. @Autowired配合@Qualifier("oracleDataService")使用
显式引用 Bean 时首字母忽略大小写

我们使用@Autowired配合@Qualifier("oracleDataService")

@Autowired()
@Qualifier("oracleDataService")
private DataService dataService;

发现程序可以正常运行了,但是如果改为下面这样

@Autowired()
@Qualifier("OracleDataService")
private DataService dataService;

会报错Unsatisfied dependency expressed through field 'dataService'.......

原因是找不到可以注入的Bean

但并不是所有的Bean都默认首字母小写的,Spring有自己的转换规则

BeanNameGenerator#generateBeanName 即用来产生 Bean 的名字

默认实现是:如果一个类名是以两个大写字母开头的,则首字母不变,其它情况下默认首字母变成小写

为了避免此类隐式规则推荐在定义Bean时就手动指定命名

@Repository("SQLiteDataService")
@Slf4j
public class SQLiteDataService implements DataService {
//省略实现
}
引用内部类的 Bean 遗忘类名
@RestController
public class StudentController {
	@Repository
	public static class InnerClassDataService implements DataService{
        @Override
        public void deleteStudent(int id) {
        //空实现
        }
    }
}

这时候我们要注入InnerClassDataService时,要按照以下格式:

@Autowired
@Qualifier("studentController.InnerClassDataService")
private DataService innerClassDataService;
@Vlaue注解没有注入预期的值

假如我们在配置文件 application.properties 配置了这样的属性:

username=admin
password=pass

在程序里这样注入:

@RestController
@Slf4j
public class ValueTestController {
    @Value("${username}")
    private String username;
    @Value("${password}")
    private String password;
    @RequestMapping(path = "user", method = RequestMethod.GET)
    public String getUser(){
        return username + ":" + password;
    };
}

输出时发现username的值并不正确

原因是@Value在查询值的过程中,并不只查application.properties文件,包括系统环境变量systemEnvironment,系统参数 systemProperties中的同名配置也可能会被注入。

错乱注入集合
@Bean
public Student student1(){
	return createStudent(1, "xie");
}
@Bean
public Student student2(){
	return createStudent(2, "fang");
}

private Student createStudent(int id, String name) {
    Student student = new Student();
    student.setId(id);
    student.setName(name);
    return student;
}

image-20240821100040008

这样我们就可以完成List<Student> students的注入,不过这样比较麻烦,还可以用下面的方式:

image-20240821100152327

如果两种方式并存会发生什么呢,假如把第一种方式叫做收集方式,第二种方式叫做直接装配方式,程序运行的结果其实是后面的注入方式根本没有生效,只返回了收集方式的数据。

原因是:当Spring在通过收集方式找目标对象时,只要不为空就直接返回,不再进行直接装配,因此只返回了收集方式的数据。也就是说这两种装配集合的方式是不会都执行的。

解决方案

统一注入方式,只采用其中一种。

生命周期

构造器内空指针异常

image-20240821101202188

本意是想要在初始化类时执行一次检查,此时lightService已经被自动装配好,然后进行一次检查,但实际执行时其实会报空指针错误。

原因是对于Bean的生命周期来讲,先创建实例(构造器),再装配内部@Autowired的属性,最后执行后置处理函数,创建实例先于装配属性执行,因此执行时会出现空指针错误。

解决方式

  1. 构造器注入
@Component
public class LightMgrService {
    
    private LightService lightService;
    
    public LightMgrService(LightService lightService) {
        this.lightService = lightService;
        lightService.check();
    }
    
}

原因同第二个问题,在构造器中的变量,Spring创建时会去查找对应的Bean,因此完成初始化

  1. 添加 init 方法,并且使用 @PostConstruct 注解进行修饰:
@Component
public class LightMgrService {
    @Autowired
    private LightService lightService;
    @PostConstruct
    public void init() {
    	lightService.check();
    }
}
  1. 实现 InitializingBean 接口,在其 afterPropertiesSet() 方法中执行初始化代码:
@Component
public class LightMgrService implements InitializingBean {
    @Autowired
    private LightService lightService;
    @Override
    public void afterPropertiesSet() throws Exception {
    	lightService.check();
    }
}

后面两种解决方案都与Bean生命周期中的后置函数有关,在装配完属性,初始化时执行。

意外触发 shutdown 方法

这里主要是通过@Bean注解修饰的Bean,在Spring容器关闭时候会意外执行shutdown或者close方法

原因是@Bean注解修饰时会String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;destroyMethod属性一个默认值,此时 Spring 会检查当前 Bean 对象的原始类中是否有名为 shutdown 或者 close 的方法,如果有,此方法会被 Spring 记录下来,并在容器被销毁时自动执行。

解决方式

  1. 不要定义这样具有特殊意义的方法名
  2. 定义Bean时手动指定destroyMethod

image-20240821102716298

注意这里是只有@Bean修饰的Bean,@Service等不会出现这样的情况

AOP

this 调用的当前类方法无法被拦截

这个可以参考我的这篇文章:https://blog.csdn.net/qq_56517253/article/details/140553254?spm=1001.2014.3001.5501

原因是this调用时候只是一个普通对象,而不是一个增强对象,因此没有被切面拦截或者事务未生效。

解决方案

  1. 在类的内部注入自己(注意循环依赖)

image-20240821103417075

  1. 从上下文中中取得当前Bean实例
直接访问被拦截类的属性抛空指针异常

image-20240821103917983

image-20240821103947340

在定义切面进行拦截AdminUserService后,访问其adminUser属性会报空指针错误,原因是 Spring 使用 CGLIB 生成 Proxy时生成的代理对象不会初始化内部属性

解决方案

  1. 添加方法获取user对象,当代理类方法被调用,会被 Spring 拦截,从而进入intercept,并在此方法中获取被 代理的原始对象。而在原始对象中,类属性是被实例化过且存在的。因此代理类是可以通 过方法拦截获取被代理对象实例的属性。

image-20240821104819259

  1. 修改启动参数 spring.objenesis.ignore为true
错乱混合不同类型的增强

image-20240821105150433

这里实际上是针对同一个方法定义了两个增强,一个是统计方法耗时,一个是校验权限,但是最终统计耗时的结果把权限校验也算进去了。引申出的问题是当同一个切面(Aspect)中同时包含多个不同类型的增强时(Around、Before、After、AfterReturning、AfterThrowing 等),它们的执行是有顺序的。那么顺序如何确定?

这里直接写结论:spring5.3版本时,最终的排序结果依次是 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class

错乱混合同类型增强

如果对同一个方法做两个before增强,那么执行的顺序是怎样呢?

直接说结论:当同一个切面(Aspect)中同时包含多个增强时,首先会根据类型(@Around、@Before…)进行比较,接着会根据切面方法名进行比较并排序。

这些案例的出现原因和解决方式都离不开源码的解读,傅健老师牛逼!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值