Fork me on Gitee

Spring中常踩坑实记

Spring Bean的默认名称生成策略导致的空指针

我们熟悉的Bean名称生成策略

image-20230623180039315

案例

创建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

image-20230623181857595

探究SpringBean名称生成源码 AnnotationBeanNameGenerator类下的generateBeanName方法,通过查看Introspector类中的decapitalize方法,可以看到. 如果BeanName长度超过1并且名称第一二个字母均为大写。直接返回实例Name。因此上述应将Bean名称从qYImooc改为QYImooc即可。

尝试修改bean名称后重新运行该段程序

@Test
public void testDefaultBeanName(){
    QYImooc qyImooc = (QYImooc) ApplicationUtils.getBean("QYImooc");
    qyImooc.print();
}

可以观察到程序正确执行,打印输出测试内容。

image-20230623182352053

总结整理

  1. 命名类型名称,避免开头两个字母大写
  2. getBean时,无法避开开头两个字母大写,使用正确的bean指定策略
  3. 在@Service注解上指定Bean名称,如@Service("qYImooc")
  4. 通过类型获取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定义在主包的外面。

image-20230623182923861

第一类错误案例代码

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();
}

第三类错误,将待注入的包定义在启动类扫描范围之外,SpringEscapeApplicationcom.imooc.spring.escape中,而outer类在com.imooc.spring.outer

image-20230623191413823

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注解的理解

image-20230623190303980

  • value:指定需要扫描的包范围
  • includeFilters:按照配置的规则,只去扫描需要的
  • excludeFilters:按照配置的规则,排除扫描的Bean
  • lazyInit:懒加载,使用的时候才会被Spring初始化

不适用自动注入你还会获取上下文吗?

对Spring容器和应用上下文的理解

  • Spring容器的核心是负责管理对象,且并不只是帮我们创建对象,它负责了对象整个生命周期的管理-创建、装配、销毁
  • 应用上下文可以认为是Spring容器的一种实现,也就是用于操作容器的容器类对象

image-20230623195205559

Spring核心是容器,但是容器并不唯一

image-20230623195407418

获取应用上下文(ApplicationContext)的四种方式

image-20230623195542414

  1. 通过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.");
    }
}

image-20230623202330954

  1. 通过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.");
    }
}

image-20230623202437250

image-20230623200509175

  1. 通过ApplicationContextStore
ApplicationContextStore.setApplicationContext(
                SpringApplication.run(SpringEscapeApplication.class,args)
        );

由于SpringApplication.run本身就是返回的就是ConfigurableApplicationContext方式。

  1. ApplicationContextAware获取

image-20230623201028021

@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.");
    }
}

image-20230623202650872

多线程下Spring Bean的数据不符合预期怎么办

Spring 默认的是单例Bean

image-20230623234500963

案例: 编写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());
}


image-20230624000934432

从运行结果中可以看出来, Spring默认是单例模式Bean,是无状态的。

Spring 提供的Bean Scope

image-20230624000020276

在DefaultImoocManagerService上新增注解@Scope(BeanDefinition.SCOPE_PROTOTYPE),再次运行上面的测试用例。运行结果表明实例化的Bean是独占的。

image-20230624001158403

对单例Bean的思考

image-20230624000421720

你是不是经常报存在多个可用Bean异常

与Spring Bean相关的注解

  • @Autowire: 属于Spring框架,默认使用类型(byType)进行注入
  • @Qualifier: 结合@Autowired一起使用,自动注入策略由byType变成byName
  • @Resource: Java EE自带的注解,默认按byName自动注入
  • @Primary:存在多个相同类型的Bean,则@Primary用于定义首选项

image-20230703220821415

案例:使用不同序列化 RedisTemplateBean,根据不同的注解得到不同的注入结果

<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

RedisConfig配置类,配置两个Bean名称分别为qinyiRedisTemplateimoocRedisTemplate, 连同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值为二进制存储。

image-20230705001452286

  • 测试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值为字符串存储。

image-20230705001755827

  • 测试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值为二进制存储。与预期一致

image-20230705002011559

  • 测试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值为字符串存储。与预期一致。

image-20230705002429039

关于Bean注入常见的两类错误

  • 只定义了接口,但是没有实现,抛出NoSuchBeanDefinitionException异常
  • 定义了接口的多个实现类,只使用@Autowired实现注入,抛出NoUniqueBeanDefinitionException异常

image-20230705002950501

  1. 针对与定义了接口,没有实现抛出的异常,可以在@Autowired中使用required=false,暂时允许Bean为空,但最应处理的还是对接口进行实现。
  2. 针对与@Autowired实现注入,但是接口的多个实现类,第一种解决方案是加入@Qualifier指明需要注入的Bean名称。或者根据byName,使用Bean的名称如(private ITemplateManagerService qinyiTemplateManagerService;),指定接口的实现类为qinyiTemplateManagerService

Spring Bean出现了循环依赖,该怎么办?

关于循环依赖,你需要知道

  • 循环依赖指的是多个对象之间的依赖关系形成一个闭环

image-20230705205230416

创建两个ServiceQinyiJavaServiceImoocCourseService,两个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?

image-20230705220725669

对注入方式由构造函数方式改为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解决,程序正常输出。

image-20230705221136281

除上述采用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 使用三级缓存策略解决循环依赖问题

image-20230705214902812

如上图,再第一步,通过使用构造方式进行实例化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);

image-20230705215411742

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)

image-20230705232726822

常见的事务失效的场景

  1. 主动捕获了异常,导致事务不能回滚。
  2. 不是Unchecked异常,事务不能回滚。(抛出自定义Customer异常,Customer异常继承Exceptioon)
  3. 同一个类中,一个不标注事物的方法去调用了事务的方法,事务会失效
  4. 方法被非public修饰符修饰(protected/private)
  5. 数据库引擎本身不支持事务

针对于第一种异常场景,可以手动标记回滚

@Transactional
public void CatExceptionCanNotRollback() {
    try{
        idAndNameDao.save(new IdAndName("qinyi"));
        throw new RuntimeException();

    }catch (Exception e){
        e.printStackTrace();
        // 可以 手动标记回滚
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}
posted @ 2023-07-05 00:34  shine-rainbow  阅读(43)  评论(0编辑  收藏  举报