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,但是却提供了多个
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注解),因此无法正常运行。
解决方案
- 给其中一个Bean加上@Primary注解,让其优先级更高
- 不要使用
private DataService dataService
,而是将命名改为bean的名字private DataService oracleDataService
@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;
}
这样我们就可以完成List<Student> students
的注入,不过这样比较麻烦,还可以用下面的方式:
如果两种方式并存会发生什么呢,假如把第一种方式叫做收集方式,第二种方式叫做直接装配方式,程序运行的结果其实是后面的注入方式根本没有生效,只返回了收集方式的数据。
原因是:当Spring在通过收集方式找目标对象时,只要不为空就直接返回,不再进行直接装配,因此只返回了收集方式的数据。也就是说这两种装配集合的方式是不会都执行的。
解决方案
统一注入方式,只采用其中一种。
生命周期
构造器内空指针异常
本意是想要在初始化类时执行一次检查,此时lightService
已经被自动装配好,然后进行一次检查,但实际执行时其实会报空指针错误。
原因是对于Bean的生命周期来讲,先创建实例(构造器),再装配内部@Autowired
的属性,最后执行后置处理函数,创建实例先于装配属性执行,因此执行时会出现空指针错误。
解决方式
- 构造器注入
@Component
public class LightMgrService {
private LightService lightService;
public LightMgrService(LightService lightService) {
this.lightService = lightService;
lightService.check();
}
}
原因同第二个问题,在构造器中的变量,Spring创建时会去查找对应的Bean,因此完成初始化
- 添加 init 方法,并且使用
@PostConstruct
注解进行修饰:
@Component
public class LightMgrService {
@Autowired
private LightService lightService;
@PostConstruct
public void init() {
lightService.check();
}
}
- 实现
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 记录下来,并在容器被销毁时自动执行。
解决方式
- 不要定义这样具有特殊意义的方法名
- 定义
Bean
时手动指定destroyMethod
注意这里是只有@Bean修饰的Bean,@Service等不会出现这样的情况
AOP
this 调用的当前类方法无法被拦截
这个可以参考我的这篇文章:https://blog.csdn.net/qq_56517253/article/details/140553254?spm=1001.2014.3001.5501
原因是this调用时候只是一个普通对象,而不是一个增强对象,因此没有被切面拦截或者事务未生效。
解决方案
- 在类的内部注入自己(注意循环依赖)
- 从上下文中中取得当前Bean实例
直接访问被拦截类的属性抛空指针异常
在定义切面进行拦截AdminUserService
后,访问其adminUser
属性会报空指针错误,原因是 Spring 使用 CGLIB 生成 Proxy时生成的代理对象不会初始化内部属性。
解决方案
- 添加方法获取user对象,当代理类方法被调用,会被 Spring 拦截,从而进入
intercept
,并在此方法中获取被 代理的原始对象。而在原始对象中,类属性是被实例化过且存在的。因此代理类是可以通 过方法拦截获取被代理对象实例的属性。
- 修改启动参数 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…)进行比较,接着会根据切面方法名进行比较并排序。
这些案例的出现原因和解决方式都离不开源码的解读,傅健老师牛逼!