Spring编程常见错误50例-Spring Bean定义常见错误

隐式扫描不到Bean的定义

问题

当启动类放置在com.psj.A包中,控制器类放置在com.psj.B包中时,启动SpringBoot后是扫描不到控制器的


原因

SpringBoot的默认扫描规则是扫描启动类所在的包及其子包中是否存在控制器


解决方式

@SpringBootApplication
// @ComponentScan("com.psj.B")  // 显式指定其它包,原来的默认扫描包就被忽略,即扫描不到com.psj.A下的控制器
@ComponentScans(value = { @ComponentScan(value ="com.psj.B") })
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

定义的Bean缺少隐式依赖

问题

运行下面代码会出现错误:Parameter 0 of constructor in com.psj.demo.service.impl.ServiceImpl required a bean of type 'java.lang.String' that could not be found.

@Service
public class ServiceImpl {
     private String serviceName;
     public ServiceImpl(String serviceName){
         this.serviceName = serviceName;
  }
}

原因

  • 当创建一个Bean时调用的方法是AbstractAutowireCapableBeanFactory#createBeanInstance,它主要包含两大基本步骤:

    • 寻找构造器:Spring会先执行determineConstructorsFromBeanPostProcessors方法来获取构造器

    • 通过反射调用构造器创建实例:通过autowireConstructor方法带着构造器去创建实例

      • 调用instantiate方法时需要获取参数argsToUse
      • argsToUse通过调用createArgumentArray方法来构建调用构造器的参数数组,该方法的最终实现是从BeanFactory中获取Bean
      argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
      								getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
      
  • 定义一个类为 Bean后如果再显式定义构造器,则该Bean在构建时会自动根据构造器参数定义寻找对应的Bean,然后反射创建这个Bean


解决方式

// 在启动类中添加下面代码
@Bean
public String serviceName() {
    return "psj";
}

原型Bean被固定

  • 在Spring框架中,Bean的生命周期由实例化、属性赋值和初始化/销毁三个主要阶段组成,而Bean的作用域是用来定义Bean的生命周期

  • Spring中的原型Bean(prototype Bean)是一种作用域,意味着每次需要一个新的Bean时,Spring都会创建一个新的实例。这和单例Bean(singleton Bean)不同,单例Bean意味着无论你请求多少次,Spring都只创建一个Bean实例。

  • 在Spring中可通过在Bean的配置类上添加@Scope("prototype")注解或在XML配置文件中设置元素的scope属性为prototype将Bean的作用域设置为原型

  • 使用原型Bean的主要好处是减少对象创建和销毁的开销(每次创建新的Bean实例则不需要额外的内存保存已创建的实例,也不需要额外的开销来处理这些实例的销毁),在处理大量对象或需要频繁创建新实例的情况下尤其有用。缺点是增加了应用程序的内存消耗(每个请求都会创建新的Bean实例)

  • 当使用原型Bean时,如果Bean中包含任何依赖则每次请求新的Bean实例时,这些依赖也需被重新创建


问题

通过下面代码定义Bean为原型Bean后,每次访问服务使用的Bean都是一样的,相当于创建的是单例Bean:

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {}
@RestController
public class HelloWorldController {
     @Autowired
     private ServiceImpl serviceImpl;
     @RequestMapping(path = "hi", method = RequestMethod.GET)
     public String hi(){
        return "helloworld, service is : " + serviceImpl;
     };
}

原因

  • 当属性成员serviceImpl声明为@Autowired后,在创建HelloWorldController Bean时,会先使用构造器反射出实例,然后来装配各个标记为 @Autowired的属性成员

  • 在执行过程会使用很多BeanPostProcessor来做完成工作,其中一种是AutowiredAnnotationBeanPostProcessor,它会寻找到ServiceImpl类型的

    Bean然后设置给对应serviceImpl成员

  • 由于field.set()方法的执行只发生了一次,后续就固定起来了,它并不会因为ServiceImpl标记了SCOPE_PROTOTYPE而改变

@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    Field field = (Field) this.member;
    Object value;
    // 寻找bean
    if (this.cached) {
        try {
            value = resolveCachedArgument(beanName, this.cachedFieldValue);
        }
        catch (BeansException ex) {
            // Unexpected target bean mismatch for cached argument -> re-resolve
            this.cached = false;
            logger.debug("Failed to resolve cached argument", ex);
            value = resolveFieldValue(field, bean, beanName);
        }
    }
    else {
        value = resolveFieldValue(field, bean, beanName);
    }
    if (value != null) {
		// 将bean设置给成员字段
        ReflectionUtils.makeAccessible(field);
        field.set(bean, value);
    }
}
  • 当一个单例的 Bean,使用autowired注解标记其属性时,该属性值会被固定下来

解决方式

// 思想:保证每次访问接口都会创建新的Bean
@RestController
@Slf4j
public class HelloWorldController {
    @Autowired
    private ApplicationContext applicationContext;

    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(){
         return "helloworld, service is : " + getServiceImpl();
    };

    // 方法1:自动注入Context
    public ServiceImpl getServiceImpl(){
    	return applicationContext.getBean(ServiceImpl.class);
    }
    
    // 方法2:使用@Lookup,通过该注解产生一个CGLIB子类,即创建一个新的Bean
    // 该方法体中的内容并不会被执行,内容是什么并不重要
    @Lookup
    public ServiceImpl getServiceImpl(){
        
        log.info("executing this method");
        return null;
    }
}

参考

极客时间-Spring 编程常见错误 50 例

你可能感兴趣的:(框架学习,spring,java,源码分析)