> 文档中心 > Spring 定义错误案例分析

Spring 定义错误案例分析

文章目录

      • Spring 的核心
      • 案例1:隐式扫描不到 Bean 的定义
      • 案例2:定义的 Bean 缺少隐式依赖
      • 案例3:原型 Bean 被固定
      • 总结
      • 补充
        • 1、为什么对象要有 getter、setter 方法
        • 2、什么情况下需要使用单例?
      • ✨todoList

以下为学习极客空间《Spring 编程常见错误 50 例》学习笔记整理,原课程课程链接 十分值得推荐,欢迎了解~

Spring 的核心

    Spring 的两大核心:IOC、AOP,IOC 是由扫描器自动发现哪些类需要成为 bean,然后实例化成 bean(只能通过反射了);AOP 是拦截方法调用,进行一些扩展,这里也是用的反射,对代理对象进行增强后返回。
    Spring 的简单易用得益于 “约定大于配置”。

案例1:隐式扫描不到 Bean 的定义

👾 问题:
    刚接触 Spring 时可能有遇到这个约定——启动类和需要成为 baan 的类要写在同一个包下,比如某个 controller 和启动类不在一个包里,就会发现访问 controller 无效的情况。
🐾分析:
    关键点在于 @SpringBootApplication 注解,继承了另外一些注解,具体定义如下:
Spring 定义错误案例分析
Spring 定义错误案例分析
     其中ComponentScan,就是用来扫描定义的 bean 的,它的 basePackages没有指定,所以默认为空,再看org.springframework.context.annotation.ComponentScanAnnotationParser,其中 parse 方法,debug 模式可以看到,当 basePackages 为空时,实际上扫描的是启动类所在的包。
在这里插入图片描述

     接下来解决问题,当然不只是把 controller 移动到和启动类同一个包下就完事儿😂,而是应该真正满足需求——让启动类能够扫描到 controller (思路:默认情况下,扫描的是启动类所在的包,想让 controller 被扫描到,一种是让 controller位于启动类所在的包中,指标不治本~ 第二种是指定,而不走默认情况,让启动类能够扫描到 controller )
🕶️ 解决方法:
     显式声明启动类扫描包,如:

@ComponentScan("com.example.demo.controller")

(发现 IDEA 还挺智能的~,如果这里指定的是启动类所在的包,会报错:Redundant declaration: @SpringBootApplication already applies given @ComponentScan)
(说回来,启动类没必要像 controller、service、dao、entity 一样各搞一个包,就放在共有的包就好了,让以上都能扫描到)
     不过这样一来,原先默认的扫描范围,即 启动类所在的包,就不会被添加进去了,如果想一并生效,可以使用多个 @ComponentScan,也可以使用 @ComponentScans。
    

案例2:定义的 Bean 缺少隐式依赖

👾 问题:
     有时候,除了默认的无参构造,我们可能也需要把某些参数传进来定义对象,比如:

@Servicepublic class ServiceImpl implements DemoService {    private String serviceName; // 有参构造    public ServiceImpl(String serviceName) { this.serviceName = serviceName;    }

(其实写出这种代码的下一步,就应该会想到说 怎么给这个参数 serviceName 赋值了,毕竟已经再用 Spring 了,就不像之前无它时,想 new 就 new 随时随地 new 了😆)
     ServiceImpl 类,用了 @Service 注解,也就成了一个 bean,然后使用这个 bean:

@RestController@RequestMapping("/demo")public class DemoController {    @Autowired    DemoService demoService;@RequestMapping("/getScope")    public String getScope(){ return demoService.toString();    }

运行报错:
Spring 定义错误案例分析
    报错大概是说 ServiceImpl 的构造器需要一个 String 类型的参数,但实际上没找到。
🐾分析:
     当创建一个 Bean 时,调用的方法是org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory #createBeanInstance。它主要包含两大基本步骤:寻找构造器和通过反射调用构造器创建实例。 关键代码:

    Constructor<?>[] ctors = this.determineConstructorsFromBeanPostProcessors(beanClass, beanName);      if (ctors == null && mbd.getResolvedAutowireMode() != 3 && !mbd.hasConstructorArgumentValues() && ObjectUtils.isEmpty(args)) {   ctors = mbd.getPreferredConstructors();   return ctors != null ? this.autowireConstructor(beanName, mbd, ctors, (Object[])null) : this.instantiateBean(beanName, mbd);      } else {   return this.autowireConstructor(beanName, mbd, ctors, args);      }

     其中 determineConstructorsFromBeanPostProcessors 用来获取构造器,然后通过 autowireConstructor 方法,带着构造器 ctors 去创建实例,而这一步,不仅需要构造器,还需要构造器对应的参数 args··。 (待验证:这个报错其实也说明 bean默认是没有无参构造的,否则不传有参构造的参数,应该也能创建实例才对 补充:当只有一个有参构造时,是不会调用无参构造的;但是如果有多个有参构造可供调用,Spring 无从选择,将会尝试去调用默认构造;但是默认构造是不存在的,所以这种情况会报错。
     既然在用 Spring 了,那就不直接显式使用 new 了,只能寻找依赖作为构造器调用参数。
     至于这个构造器参数的获取,关键代码见 org.springframework.beans.factory.support.ConstructorResolver#autowireConstructor,其中:

   argsHolder = this.createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames, this.getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);

     这里的 createArgumentArray 方法,就是用来构建构造器参数的,最终是会到 beanFactotry 中获取到 bean,代码看不出啥名堂,在此就不罗列了。案例中目前没有 serviceName 实例,也就报错了。
    
🕶️ 解决方法:
     在 ServiceImpl 里定义一个能让 Spring 装配给 ServiceImpl 构造器参数的 bean,比如:

    @Bean    public String serviceName() { return "hello";    }

    这时又会报新的错误,循环依赖:
Spring 定义错误案例分析
     这是因为这个写法,会导致 ServiceImpl 的构造,依赖于 ServiceName,而 ServiceName 又需要先有 ServiceImpl ,尴尬了😅。给挪到 Controller 层也是一样的报错,因为 Controller 依赖了 ServiceImpl ,而 ServiceImpl 的构造,依赖于 ServiceName,而 ServiceName 又需要先有 Controller,循环得更深了:Spring 定义错误案例分析
     想要打破循环,重新定义一个类,用 @Component标识,然后再注册 ServiceImpl 即可:(待验证:这里方法名使用 getServiceName 也可,而且如果 serviceName() 和 getServiceName()方法同时存在,前者优先,这是什么原理?使用@Bean 作用在方法上,不指定 bean 名称的话,bean的命名规则是怎样的?

  • 感觉对应的 beanName 应该就是 serviceName 和 getServiceName;装配时若找到前者,就用不到后者了,因为我声明了名称和方法名相反的bean:
@Componentpublic class ServiceNameClass {    @Bean("serviceName")    public String getServiceName() { return "hello";    }    @Bean("getServiceName")    public String serviceName() { return "123";    }}

结果输出的是 hello,说明 xxx 是比getxxx优先的。包括把两个方法颠倒也不影响的。而如果两个bean名称相同,似乎是哪个 bean 写在前面,就先
只是为什么 getXXX 的 bean 也能做装配,这里不太理解…

     以上是一个有参构造的情况,如果是多个呢:

@Servicepublic class ServiceImpl {    private String serviceName;    public ServiceImpl(String serviceName){ this.serviceName = serviceName;    }    public ServiceImpl(String serviceName, String otherStringParameter){ this.serviceName = serviceName;    }}

     如果我们仍用非 Spring 的思维去审阅这段代码,可能不会觉得有什么问题,毕竟 String 类型可以自动装配了,无非就是再增加了一个 String 类型的参数otherStringParameter 而已。但是如果了解 Spring 内部是用反射来构建 Bean 的话,就不难发现问题所在:存在两个构造器,都可以调用时,到底应该调用哪个呢?最终 Spring 无从选择,只能尝试去调用默认构造器,而这个默认构造器又不存在,所以测试这个程序它会出错。
    

案例3:原型 Bean 被固定

👾 问题:
     再来看个 Bean 定义不生效的例子,使用原型 Bean:

@Service@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)public class ServiceImpl {}

使用它:

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

     结果发现,不管访问多少次这个路径,访问的结果都是不变的,跟个单例似的,这与它定义为原型 Bean 的 初衷背道而驰。
🐾分析:
     当一个属性成员 serviceImpl 声明为 @Autowired 之后,那么在创建 HelloWorldController 这个 Bean 时,会先使用构造器反射出这个实例,然后再装配各个标记为 @Autowired 的属性成员,具体的执行过程,会使用到很多 BeanPostProcessor 来完成工作,其中相关的是 AutowiredAnnotationBeanPostProcessor,它会通过 DefaultListableBeanFactory#findAutowireCandidates 寻找到 ServiceImpl 类型的 Bean,然后赋值给 serviceImpl 成员。【为啥删掉呢,因为这个类里我没找到这个方法:)】 关键步骤见org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredMethodElement#inject:

  protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {     if (!this.checkPropertySkipping(pvs)) {  Method method = (Method)this.member;  Object[] arguments;  if (this.cached) {      try {   arguments = this.resolveCachedArguments(beanName);      } catch (NoSuchBeanDefinitionException var8) {   arguments = this.resolveMethodArguments(method, bean, beanName);      }  } else {      arguments = this.resolveMethodArguments(method, bean, beanName);  }  if (arguments != null) {      try {   ReflectionUtils.makeAccessible(method);   method.invoke(bean, arguments);      } catch (InvocationTargetException var7) {   throw var7.getTargetException();      }  }     } }

     可以看到,第一次HelloWorldController自动注入的时候,会通过反射机制设置给对应的 field:serviceImpl,这个 field 的执行只发生一次,所以后续就固定起来了,并不会因为 ServiceImpl 标记了 SCOPE_PROTOTYP 而改变。也就是说,只要程序一启动,HelloWorldController 里的 serviceImpl 是固定的,@Scope并不起作用。
  
🕶️ 解决方法:
     破除这个“固定”的封印即可,使得每次使用时,都会重新获取一次属性,方法如下:
(1)自动注入 Context
    定义个 getServiceImpl() 方法,通过方法能够获取到一个新的 ServiceImpl 类实例:

     @Autowired     private ApplicationContext applicationContext;     public ServiceImpl getServiceImpl(){  return applicationContext.getBean(ServiceImpl.class);     }

(待验证:尚且不知道applicationContext.getBean的原理)
(2)使用 Lookup 注解
     也是定义个 getServiceImpl() 方法,使用 @Lookup 注解:

   @Lookup    public ServiceImpl getServiceImpl3(){ return null;    }

     使用 @Lookup 注解的方法,具体实现是无所谓的,甚至在方法里打印日志,执行起来也并不会打印,只需知道 使用@Lookup 会使用 CGLIB (CGLIB:对代理对象类生成的 class 文件加载进来,通过修改其字节码生成子类来进行代理)实现动态代理,还有个异曲同工的方法:
(3)使用 scope注解的proxyMode:

@Service@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS, value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)public class ServiceImpl {}

     这样注入到 controller 的 bean 也是代理对象来得,每次都会从beanfactory 里面重新拿来。

总结

  • @SpringBootApplication 继承了一些注解,其中 @ComponentScan 是用来扫描定义的 bean 的,若未指定包名,默认只扫描启动类所在的包;而如果显式指定其他包,原来的默认包就被忽略了。
    • @ComponentScan可以多个同时使用,且都生效。效果等同于 @ComponentScans。
          
  • 当只有一个有参构造时,Spring 创建该类实例时是不会调用无参构造的,并且需要提供构造方法的参数实例以供装配;
    而如果有多个有参构造可供调用,Spring 无从选择,将会尝试去调用默认无参构造,但是默认构造是不存在的,所以会报错。∴ 不要提供多个有参构造
        
  • 单例对象的 @Autowired 属性一定是单例的,即使用了@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 也还是单例,因为自程序启动后只会装配一次。
    • 解决方法:使用applicationContext.getBean 或 @Lookup、或 proxyMode = ScopedProxyMode.TARGET_CLASS,每次都重新获取一次属性。
      • @Lookup 作用在方法上,使得该方法返回值使用 CGLIB 动态代理生成,方法实现不重要 ,随便怎么写都行,应该走不到这里来。
            

补充

想到啥记录啥~

1、为什么对象要有 getter、setter 方法?

     在实例方法中有一类特殊的方法,它们一般不包含任何业务逻辑,仅仅是为类成员属性提供读取和修改的方法(really?好像在业务代码里见过夹带私货的…😥),这样设计的好处:
(1)满足面向对象语言封装的特性。尽可能将属性定义为 private,针对属性值的访问与修改需要使用对应的 getter 和 setter 方法,而不是直接对 publice 的属性进行读取和修改。
(2)有利于统一控制。虽然直接对属性进行读取和修改的方式 和 使用对应的 getter 与 setter 方法在效果上是一样的,但是前者难以应对业务的变化;比如 业务要求对某个值的修改要增加统一的权限控制,如果有 setter 作为统一的属性修改方法,则更容易实现,这种情况在一些使用反射的框架中作用尤其明显。(思考这个问题,可以从方法和直接赋值的区别去考虑,方法有返回值、访问权限控制符、入参,而且方法的存在相当于一个收口,方法声明了之后可以到处使用~)
     顺便提一下,同时定义了 isXXX() 和 getXXX() ,在 iBatis、JSON序列化等场景下容易引起冲突,比如 iBatis 通过反射机制解析加载属性的 getter 方法时,首先会获取对象的所有方法,任何筛选出以 get 和 is 开头的方法,并存储到类型为 HashMap 的 getMethods 变量中,其中 key 为 属性名称,value 为 getter 方法,这样的话 isXXX() 和 getXXX() 只能保留一个,哪个方法被存储到 getMethods 变量中,就会保存哪个方法,具有一定的随机性,当两个方法定义不同时,可能导致误用哪个,进而产生问题。(启示:某个字段出问题,特别是 Boolean 类型的,可能就是获取方法有冲突,而实际只保留一个导致的,不失为一个排查思路)
    

2、什么情况下需要使用单例?

     像注册表设置(registry setting)对象、线程池、数据库连接池,这些都是共享的资源,但是只能有一个实例,如果制造出多个实例,就会导致一些问题:资源使用过量、结果不一致等;而且资源也比较珍贵,重用更佳。启示:思考xxx存在的意义这种问题,可以从必要性和优化的角度去考虑,使用xxx是否必要?是否更优?
     按说,一个类只存在一个实例,只用 Java 的静态变量(静态变量是类的所有实例共享的,它属于类,不属于任何独立的对象)就能做到,但是如果用静态变量的话,有个缺点:要想把对象赋值给一个静态变量,就得一开始创建好对象,万一创建对象这一步非常耗费资源,而程序在这次执行过程中一直没有执行到它,不就形成浪费了吗,所以单例模式应运而生,需要时才去创建。

✨todoList

  • IDEA 报错是啥原理?
  • 本文待验证项。