Spring Core常见错误及解决方案

Spring Core常见错误及解决方案

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

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

Bean定义

隐式扫描不到Bean的定义

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

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

解决方案

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

定义的 Bean 缺少隐式依赖

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

解决方案

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

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

原型 Bean 被固定

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

解决方案

每次从Context中取。

依赖注入

过多赠予,无所适从

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

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

java 复制代码
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");
    }
}

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

java 复制代码
@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")

java 复制代码
@Autowired()
@Qualifier("oracleDataService")
private DataService dataService;

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

java 复制代码
@Autowired()
@Qualifier("OracleDataService")
private DataService dataService;

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

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

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

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

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

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

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

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

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

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

properties 复制代码
username=admin
password=pass

在程序里这样注入:

java 复制代码
@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中的同名配置也可能会被注入。

错乱注入集合
java 复制代码
@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;
}

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

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

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

解决方案

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

生命周期

构造器内空指针异常

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

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

解决方式

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

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

  1. 添加 init 方法,并且使用 @PostConstruct 注解进行修饰:
java 复制代码
@Component
public class LightMgrService {
    @Autowired
    private LightService lightService;
    @PostConstruct
    public void init() {
    	lightService.check();
    }
}
  1. 实现 InitializingBean 接口,在其 afterPropertiesSet() 方法中执行初始化代码:
java 复制代码
@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

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

AOP

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

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

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

解决方案

  1. 在类的内部注入自己(注意循环依赖)
  1. 从上下文中中取得当前Bean实例
直接访问被拦截类的属性抛空指针异常

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

解决方案

  1. 添加方法获取user对象,当代理类方法被调用,会被 Spring 拦截,从而进入intercept,并在此方法中获取被 代理的原始对象。而在原始对象中,类属性是被实例化过且存在的。因此代理类是可以通 过方法拦截获取被代理对象实例的属性。
  1. 修改启动参数 spring.objenesis.ignore为true
错乱混合不同类型的增强

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

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

错乱混合同类型增强

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

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

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

相关推荐
冰敷逆向21 小时前
京东h5st纯算分析
java·前端·javascript·爬虫·安全·web
Coder_preston21 小时前
Java集合框架详解
java·开发语言
多多*21 小时前
2026年最新 测试开发工程师相关 Linux相关知识点
java·开发语言·javascript·算法·spring·java-ee·maven
树码小子21 小时前
SpringIoC & DI (1):IOC介绍 & Spring IoC使用 & DI
java·后端·spring
tb_first1 天前
万字超详细苍穹外卖学习笔记5
java·数据库·spring boot·笔记·学习·spring
铁蛋AI编程实战1 天前
ChatWiki 开源 AI 文档助手搭建教程:多格式文档接入,打造专属知识库机器人
java·人工智能·python·开源
Hx_Ma161 天前
SpringBoot消息转换器扩展fastjson
java·spring boot·spring
Coder_preston1 天前
Spring/Spring Boot实战:从入门到项目部署
java·spring boot·spring
山岚的运维笔记1 天前
SQL Server笔记 -- 第16章:MERGE
java·笔记·sql·microsoft·sqlserver
Andy Dennis1 天前
一文漫谈设计模式之创建型模式(一)
java·开发语言·设计模式