Spring中常踩坑实记
Spring Bean的默认名称生成策略导致的空指针
我们熟悉的Bean名称生成策略
案例
创建QYIMooc并用@Service进行标注
/**
* Spring Bean的默认名称生成策略
* @author zhangshao
* @date 2023/6/23 18:01
*/
@Service
public class QYImooc {
public void print(){
System.out.println("QYImooc");
}
}
编写SpringBeanUtil工具类
@Slf4j
@Component
public class ApplicationUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
if (ApplicationUtils.applicationContext == null) {
ApplicationUtils.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return ApplicationUtils.applicationContext;
}
/**
* <h2>通过 name 获取 Bean</h2>
* */
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* <h2>通过 class 获取 Bean</h2>
* */
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* <h2>通过 name + class 获取 Bean</h2>
* */
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
测试代码:通过BeanUtil按照Bean名称获取对象,并调用对象方法。
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestQYImooc {
@Test
public void testDefaultBeanName(){
QYImooc qyImooc = (QYImooc) ApplicationUtils.getBean("qYImooc");
qyImooc.print();
}
}
运行报错:原因找不到Bean
探究SpringBean名称生成源码 AnnotationBeanNameGenerator
类下的generateBeanName方法,通过查看Introspector
类中的decapitalize方法,可以看到. 如果BeanName长度超过1并且名称第一二个字母均为大写。直接返回实例Name。因此上述应将Bean名称从qYImooc
改为QYImooc
即可。
尝试修改bean名称后重新运行该段程序
@Test
public void testDefaultBeanName(){
QYImooc qyImooc = (QYImooc) ApplicationUtils.getBean("QYImooc");
qyImooc.print();
}
可以观察到程序正确执行,打印输出测试内容。
总结整理
- 命名类型名称,避免开头两个字母大写
- getBean时,无法避开开头两个字母大写,使用正确的bean指定策略
- 在@Service注解上指定Bean名称,如@Service("qYImooc")
- 通过类型获取Bean对象,如下面的范例
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestQYImooc {
@Test
public void testDefaultBeanName() {
// QYImooc qyImooc = (QYImooc) ApplicationUtils.getBean("QYImooc");
QYImooc qyImooc = ApplicationUtils.getBean(QYImooc.class);
qyImooc.print();
}
}
使用了@Autowired注解,但是仍然出现了空指针
不理解Spring的自动装配规则,错误的使用new
- 第一类错误:属性对象虽然注入了,但是当前类没有被标记为Spring Bean
- 第二类错误:当前类标记为Spring Bean,但属性对象也注入了。但是,却使用了new去获取类对象
正确的做法:使用Bean的整个过程,都应该被Spring容器所管理
没有理解Spring默认的包扫描机制,扫描不到定义的Bean
- 第三类错误:Spring默认的包扫描机制是当前包自己子包下的所有目录,但是却把Bean定义在主包的外面。
第一类错误案例代码
public class HowToUseAutowire {
@Autowired
private QYImooc imooc;
public void print(){
imooc.print();
}
}
测试代码中直接通过new方式使用
@Test
public void firstTryTest(){
assert ApplicationUtils.getApplicationContext().containsBean("qYImooc");
HowToUseAutowire howToUseAutowire = new HowToUseAutowire();
howToUseAutowire.print();
}
由于HowToUseAutowire
类未被Spring管理,报NPE
第二类错误案例代码, 测试代码同第一类错误案例。
@Component
public class HowToUseAutowire {
@Autowired
private QYImooc imooc;
public void print(){
imooc.print();
}
}
尽管HowToUseAutowire
类被Spring管理(@Component注解),但引入方式采用new,导致报NPE
正确案例,使用@Component注解并使用BeanUitl从Spring容器中获取Bean
@Test
public void thirdTryTest(){
assert ApplicationUtils.getApplicationContext().containsBean("qYImooc");
HowToUseAutowire useAutowire = ApplicationUtils.getBean(HowToUseAutowire.class);
useAutowire.print();
}
第三类错误,将待注入的包定义在启动类扫描范围之外,SpringEscapeApplication
在com.imooc.spring.escape
中,而outer类在com.imooc.spring.outer
中
Outer类代码
@Component
public class Outer {
public void print(){
System.out.println("This is An Outer Class");
}
}
测试代码,注入outer类,并调用outer类中的print方法,无法注入。
@Test
public void fourthTryTest(){
assert ApplicationUtils.getApplicationContext().containsBean("outer");
((Outer)ApplicationUtils.getBean("outer")).print();
}
正确修改:通过在启动类SpringEscapeApplication
类上标记@ComponentScan注解,规定扫描包路径
@SpringBootApplication
@ComponentScan(value = {"com.imooc.spring.escape","com.imooc.spring.outer"})
public class SpringEscapeApplication {
public static void main(String[] args) {
SpringApplication.run(SpringEscapeApplication.class,args);
}
}
@ComponentScan注解的理解
- value:指定需要扫描的包范围
- includeFilters:按照配置的规则,只去扫描需要的
- excludeFilters:按照配置的规则,排除扫描的Bean
- lazyInit:懒加载,使用的时候才会被Spring初始化
不适用自动注入你还会获取上下文吗?
对Spring容器和应用上下文的理解
- Spring容器的核心是负责管理对象,且并不只是帮我们创建对象,它负责了对象整个生命周期的管理-创建、装配、销毁
- 应用上下文可以认为是Spring容器的一种实现,也就是用于操作容器的容器类对象
Spring核心是容器,但是容器并不唯一
获取应用上下文(ApplicationContext)的四种方式
- 通过ApplicationContextInitializer
@Slf4j
public class UseInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
assert ApplicationContextStore.getApplicationContext() == null;
ApplicationContextStore.setApplicationContext(applicationContext);
assert ApplicationContextStore.getApplicationContext() != null;
log.info("UseInitializer Done.");
}
}
- 通过ApplicationListener
通过实现ApplicationListener接口并实现onApplicationEvent方法。
@Slf4j
@Component
public class UserListener implements ApplicationListener<ApplicationContextEvent> {
@Override
public void onApplicationEvent(ApplicationContextEvent event) {
assert ApplicationContextStore.getApplicationContext() == null;
ApplicationContextStore.setApplicationContext(event.getApplicationContext());
assert ApplicationContextStore.getApplicationContext() != null;
log.info("UseListener Done.");
}
}
- 通过ApplicationContextStore
ApplicationContextStore.setApplicationContext(
SpringApplication.run(SpringEscapeApplication.class,args)
);
由于SpringApplication.run本身就是返回的就是ConfigurableApplicationContext方式。
- ApplicationContextAware获取
@Slf4j
@Component
public class UseAware implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
assert ApplicationContextStore.getApplicationContext() == null;
ApplicationContextStore.setApplicationContext(applicationContext);
assert ApplicationContextStore.getApplicationContext() != null;
log.info("UseAware Done.");
}
}
多线程下Spring Bean的数据不符合预期怎么办
Spring 默认的是单例Bean
案例: 编写Service, 在该service中模拟常见的增加Imooce操作。
@Slf4j
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class DefaultImoocManagerService {
private List<String> imoocers = null;
@PostConstruct
public void init(){
log.info("Coming In Default DefaultImoocManagerService init!");
this.imoocers = new ArrayList<>(100);
}
public void addImoocer(String imoocer){
this.imoocers.add(imoocer);
}
public int imoocerCount(){
return this.imoocers.size();
}
public List<String> getImoocers(){
return this.imoocers;
}
}
测试类:通过BeanUtil 实例化两个对象,当service02中执行添加DaMiao, 是否会因为单例模式,而导致查看service01中出现DaMiao
@Test
public void testDefaultSingleton(){
DefaultImoocManagerService service01 = ApplicationUtils.getBean(DefaultImoocManagerService.class);
DefaultImoocManagerService service02 = ApplicationUtils.getBean(DefaultImoocManagerService.class);
service01.addImoocer("KunKun");
service01.addImoocer("Consin");
log.info("Service01 has Imoocers {}",service01.getImoocers());
service02.addImoocer("DaMiao");
log.info("Service01 has Imoocers {}",service01.getImoocers());
}
从运行结果中可以看出来, Spring默认是单例模式Bean,是无状态的。
Spring 提供的Bean Scope
在DefaultImoocManagerService上新增注解@Scope(BeanDefinition.SCOPE_PROTOTYPE),再次运行上面的测试用例。运行结果表明实例化的Bean是独占的。
对单例Bean的思考
你是不是经常报存在多个可用Bean异常
与Spring Bean相关的注解
- @Autowire: 属于Spring框架,默认使用类型(byType)进行注入
- @Qualifier: 结合@Autowired一起使用,自动注入策略由byType变成byName
- @Resource: Java EE自带的注解,默认按byName自动注入
- @Primary:存在多个相同类型的Bean,则@Primary用于定义首选项
案例:使用不同序列化 RedisTemplateBean,根据不同的注解得到不同的注入结果
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
RedisConfig配置类,配置两个Bean名称分别为qinyiRedisTemplate
和imoocRedisTemplate
, 连同SpringBoot-redis-starter默认的RedisTemplate
,一共有三种名称RedisTemplateBean。
@SuppressWarnings("all")
@Configuration
public class RedisConfig {
private final RedisConnectionFactory redisConnectionFactory;
@Autowired
public RedisConfig(RedisConnectionFactory redisConnectionFactory){
this.redisConnectionFactory = redisConnectionFactory;
}
@Bean(name = "qinyiRedisTemplate")
public RedisTemplate<String,Object> getQinyiRedisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> template = new RedisTemplate<>();
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
template.setConnectionFactory(factory);
// key为String,value为String
template.setKeySerializer(stringSerializer);
template.setValueSerializer(stringSerializer);
return template;
}
@Bean(name = "imoocRedisTemplate")
public RedisTemplate<String,Object> getImoocRedisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> template = new RedisTemplate<>();
JdkSerializationRedisSerializer redisSerializer = new JdkSerializationRedisSerializer();
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
template.setConnectionFactory(factory);
// key为String,value为二进制
template.setKeySerializer(stringSerializer);
template.setValueSerializer(redisSerializer);
return template;
}
}
- 测试1 使用 @Autowired 注入
@SuppressWarnings("all")
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestMultiUsableBean {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testAutowire(){
assert imoocRedisTemplate !=null;
redisTemplate.getConnectionFactory().getConnection().flushAll();
redisTemplate.opsForValue().set("name","qinyi");
}
}
运行程序后,观察Redis中存储的key信息。发现key值为二进制,value值为二进制存储。
- 测试2:使用@Authwired,并只用@Qualifier指明需要注入的Bean名称
@SuppressWarnings("all")
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestMultiUsableBean {
@Autowired
@Qualifier("qinyiRedisTemplate")
private RedisTemplate redisTemplate;
@Test
public void testAutowire(){
assert redisTemplate !=null;
redisTemplate.getConnectionFactory().getConnection().flushAll();
redisTemplate.opsForValue().set("name","qinyi");
}
}
运行程序后,观察Redis中存储的key信息。发现key值为字符串存储,value值为字符串存储。
- 测试3:使用@Resource根据名称进行注入
@SuppressWarnings("all")
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestMultiUsableBean {
@Resource
private RedisTemplate imoocRedisTemplate;
@Test
public void testAutowire(){
assert imoocRedisTemplate !=null;
imoocRedisTemplate.getConnectionFactory().getConnection().flushAll();
imoocRedisTemplate.opsForValue().set("name","qinyi");
}
}
运行结果后,观察Redis中存储的key信息。发现key值为字符串存储,value值为二进制存储。与预期一致
- 测试4 使用@Autowired 配合使用@Primary注解指定 优先注入类型
在RedisConfig.java中在qinyiRedisTemplateBean名称上加入@Primary注解
@Primary
@Bean(name = "qinyiRedisTemplate")
public RedisTemplate<String,Object> getQinyiRedisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> template = new RedisTemplate<>();
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
template.setConnectionFactory(factory);
// key为String,value为String
template.setKeySerializer(stringSerializer);
template.setValueSerializer(stringSerializer);
return template;
}
测试类
@SuppressWarnings("all")
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestMultiUsableBean {
@Autowired
private RedisTemplate imoocRedisTemplate;
@Autowired
private ITemplateManagerService qinyiTemplateManagerService;
@Test
public void testAutowire(){
assert imoocRedisTemplate !=null;
imoocRedisTemplate.getConnectionFactory().getConnection().flushAll();
imoocRedisTemplate.opsForValue().set("name","qinyi");
}
}
运行结果后,观察Redis中存储的key信息。发现key值为字符串存储,value值为字符串存储。与预期一致。
关于Bean注入常见的两类错误
- 只定义了接口,但是没有实现,抛出NoSuchBeanDefinitionException异常
- 定义了接口的多个实现类,只使用@Autowired实现注入,抛出NoUniqueBeanDefinitionException异常
- 针对与定义了接口,没有实现抛出的异常,可以在@Autowired中使用required=false,暂时允许Bean为空,但最应处理的还是对接口进行实现。
- 针对与@Autowired实现注入,但是接口的多个实现类,第一种解决方案是加入@Qualifier指明需要注入的Bean名称。或者根据byName,使用Bean的名称如(
private ITemplateManagerService qinyiTemplateManagerService;
),指定接口的实现类为qinyiTemplateManagerService
Spring Bean出现了循环依赖,该怎么办?
关于循环依赖,你需要知道
- 循环依赖指的是多个对象之间的依赖关系形成一个闭环
创建两个ServiceQinyiJavaService
和ImoocCourseService
,两个Service中ImoocService依赖于qinyiService中的qinyiJava方法。
- QinyiJavaService
@Service
public class QinyiJavaService {
private final ImoocCourseService courseService;
@Autowired
public QinyiJavaService(ImoocCourseService courseService){
this.courseService = courseService;
}
public void qinyiJava(){
System.out.println("QinyiJavaService");
}
}
- ImoocCourseService
@Service
public class ImoocCourseService {
private final QinyiJavaService javaService;
@Autowired
public ImoocCourseService(QinyiJavaService javaService){
this.javaService = javaService;
}
public void imoocCourse(){
javaService.qinyiJava();
}
}
测试代码
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestCyclicDependency {
@Autowired
private QinyiJavaService javaService;
@Autowired
private ImoocCourseService courseService;
@Test
public void testCyclicDependency(){
javaService.qinyiJava();
courseService.imoocCourse();
}
}
运行测试用例,发现Spring无法启动,提示 Requested bean is currently in creation: Is there an unresolvable circular reference?
对注入方式由构造函数方式改为Field方式,如下。
@Service
public class ImoocCourseService {
@Autowired
private QinyiJavaService javaService;
public void imoocCourse(){
javaService.qinyiJava();
}
}
@Service
public class QinyiJavaService {
@Autowired
private ImoocCourseService courseService;
public void qinyiJava(){
System.out.println("QinyiJavaService");
}
}
再次尝试运行测试样例,发现循环依赖被Spring解决,程序正常输出。
除上述采用Field方式解决,也可以使用set方式解决。如下
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestCyclicDependency {
@Autowired
private QinyiJavaService javaService;
@Autowired
private ImoocCourseService courseService;
@Test
public void testCyclicDependency(){
javaService.qinyiJava();
courseService.imoocCourse();
}
}
Spring 使用三级缓存策略解决循环依赖问题
如上图,再第一步,通过使用构造方式进行实例化Bean 时失败,Spring就不会继续执行了。
而Spring能解决循环依赖在第二步(populate bean填充属性)。但仅在单例模式下。
- 解决单例模式下循环依赖的三级缓存
/** * 一级缓存:用于存放完全初始化好的bean */ private final Map<String,Object> singletonObjects = new ConcurrentHashMap<String,Object>(256); /** * 二级缓存:存放原始的bean对象(尚未填充属性),用于解决循环依赖 */ private final Map<String,Object> earlySingletonObjects = new HashMap<String,Object>(16); /** * 三级缓存: 存放bean工厂对象, 用于解决循环依赖 */ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String,ObjectFactory<?>>(16);
Spring原型模式下,Spring对于循环引用也无能为力,再次运行测试用例,依旧会报循环依赖。
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ImoocCourseService {
@Autowired
private QinyiJavaService javaService;
public void imoocCourse(){
javaService.qinyiJava();
}
}
使用了@Transactional注解,但是事务并没有生效
关于Spring事务
- 事务管理是业务系统开发不可或缺的一部分,Spring提供了两种事务管理机制,分别是编程式事务和s声明式事务。
- @Transcational注解可以标识在三处,
接口
、类
和类方法
上。 - @Transcational注解有很多属性,且它们许多都很重要。(isolatio、timeout、rollbackFor)
常见的事务失效的场景
- 主动捕获了异常,导致事务不能回滚。
- 不是Unchecked异常,事务不能回滚。(抛出自定义Customer异常,Customer异常继承Exceptioon)
- 同一个类中,一个不标注事物的方法去调用了事务的方法,事务会失效
- 方法被非public修饰符修饰(protected/private)
- 数据库引擎本身不支持事务
针对于第一种异常场景,可以手动标记回滚
@Transactional
public void CatExceptionCanNotRollback() {
try{
idAndNameDao.save(new IdAndName("qinyi"));
throw new RuntimeException();
}catch (Exception e){
e.printStackTrace();
// 可以 手动标记回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}