案例 1:隐式扫描不到 Bean 的定义java
Application 类定义以下:spring
package com.spring.puzzle.class1.example1.application.application
//省略 import
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
复制代码
HelloWorldController 代码以下:编程
package com.spring.puzzle.class1.example1.controller.application
//省略 import
@RestController
public class HelloWorldController {
@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi(){
return "helloworld";
};
}
复制代码
如图所示包的结构,咱们会发现这个 Web 应用失效了,即不能识别出 HelloWorldController 了。也就是说,咱们找不到 HelloWorldController 这个 Bean 了。这是为什么?数组
案例解析:markdown
要了解 HelloWorldController 为何会失效,就须要先了解以前是如何生效的。对于 Spring Boot 而言,关键点在于 Application.java 中使用了 SpringBootApplication 注解。而这个注解继承了另一些注解,具体定义以下:app
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
//省略非关键代码
}
复制代码
从定义能够看出,SpringBootApplication 开启了不少功能,其中一个关键功能就是 ComponentScan,参考其配置以下:ui
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class)
复制代码
ComponentScan扫描的位置是由ComponentScan 注解的 basePackages 属性指定的,具体可参考以下定义:this
public @interface ComponentScan {
/**
* Base packages to scan for annotated components.
* <p>{@link #value} is an alias for (and mutually exclusive with) this
* attribute.
* <p>Use {@link #basePackageClasses} for a type-safe alternative to
* String-based package names.
*/
@AliasFor("value")
String[] basePackages() default {};
//省略其余非关键代码
}
复制代码
通过调试以后,若是直接使用 SpringBootApplication 注解定义的 ComponentScan,它的 basePackages 没有指定,扫描的包会是 declaringClass 所在的包,在本案例中,declaringClass 就是 Application.class,因此扫描的包其实就是它所在的包,即 com.spring.puzzle.class1.example1.applicationspa
问题修正调试
在这里,真正解决问题的方式是显式配置 @ComponentScan。具体修改方式以下:
@SpringBootApplication
@ComponentScan("com.spring.puzzle.class1.example1.controller")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
复制代码
也可使用@ComponentScans
@SpringBootApplication
@ComponentScans(value = { @ComponentScan(value = "com.spring.puzzle.class1.example1.controller") })
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
复制代码
ComponentScans 相比较 ComponentScan 多了一个 s,支持多个包的扫描范围指定。
案例 2:定义的 Bean 缺乏隐式依赖
如下代码段,看似没有问题,实则...
@Service
public class ServiceImpl {
private String serviceName;
public ServiceImpl(String serviceName){
this.serviceName = serviceName;
}
}
复制代码
ServiceImpl 由于标记为 @Service 而成为一个 Bean。另外咱们 ServiceImpl 显式定义了一个构造器。可是,上面的代码不是永远都能正确运行的,有时候会报下面这种错误:
Parameter 0 of constructor in com.spring.puzzle.class1.example2.ServiceImpl required a bean of type 'java.lang.String' that could not be found.
复制代码
案例解析:
当建立一个 Bean 时,它主要包含两大基本步骤:寻找构造器和经过反射调用构造器建立实例。核心的代码执行,能够参考如下代码片断:
// Candidate constructors for autowiring?
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args);
}
复制代码
Spring 会先执行 determineConstructorsFromBeanPostProcessors 方法来获取构造器,而后经过 autowireConstructor 方法带着构造器去建立实例。
autowireConstructor 方法要建立实例,不只须要知道是哪一个构造器,还须要知道构造器对应的参数,这点从最后建立实例的方法名也能够看出,(即ConstructorResolver#instantiate):
private Object instantiate(
String beanName, RootBeanDefinition mbd, Constructor<?> constructorToUse, Object[] argsToUse)
复制代码
那么上述方法中存储构造参数的 argsToUse 如何获取呢?换言之,当咱们已经知道构造器 ServiceImpl(String serviceName),要建立出 ServiceImpl 实例,如何肯定 serviceName 的值是多少?
在 Spring当中,咱们不能直接显式使用 new 关键字来建立实例。Spring 只能是去寻找依赖来做为构造器调用参数。
参数获取,能够参考下面的代码片断(即 ConstructorResolver#autowireConstructor):
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
复制代码
能够调用 createArgumentArray 方法来构建调用构造器的参数数组,而这个方法的最终实现是从 BeanFactory 中获取 Bean,能够参考下述调用:
return this.beanFactory.resolveDependency(
new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);
复制代码
若是用DeBug调试,则能够看到更多的信息:
如图所示,上述的调用便是根据参数来寻找对应的 Bean,在本案例中,若是找不到对应的 Bean 就会抛出异常,提示装配失败。
问题修正:
Spring隐式规则:定义一个类为 Bean,若是再显式定义了构造器,那么这个 Bean 在构建时,会自动根据构造器参数定义寻找对应的 Bean,而后反射建立出这个 Bean。
咱们能够直接定义一个能让 Spring 装配给 ServiceImpl 构造器参数的 Bean,例如定义以下:
//这个bean装配给ServiceImpl的构造器参数“serviceName”
@Bean
public String serviceName(){
return "MyServiceName";
}
复制代码
程序运行正常。
因此,咱们在使用 Spring 时,不要总想着定义的 Bean 也能够在非 Spring 场合直接用 new 关键字显式使用,这种思路是不可取的。
参考文献:
傅健,《Spring编程常见错误50例》
本文版权归做者和掘金共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文连接。