Spring编程常见错误-Spring Core篇(1)

1,463 阅读4分钟

01|Spring Bean 定义常见错误

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

 Application 类定义如下:

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 了。这是为何?

案例解析:

要了解 HelloWorldController 为什么会失效,就需要先了解之前是如何生效的。对于 Spring Boot 而言,关键点在于 Application.java 中使用了 SpringBootApplication 注解。而这个注解继承了另外一些注解,具体定义如下:

@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,参考其配置如下:

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class)

ComponentScan扫描的位置是由ComponentScan 注解的 basePackages 属性指定的,具体可参考如下定义:

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.application

问题修正

在这里,真正解决问题的方式是显式配置 @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例》

本文版权归作者和掘金共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。