使用好 Spring,就一定要了解它的一些潜规则,例默认扫描 Bean 的范围、自动装配构造器等。通过本节案例的分析,我们也可以感受到 Spring 的很多实现是通过反射来完成的,了解了这点,对于理解它的源码实现会大有帮助。例如在案例 3 中,为什么定义了多个构造器就可能报错,因为使用反射方式来创建实例必须要明确使用的是哪一个构造器。
spring boot 开发项目时,构建一个简单的web 程序示例如下:
QuestionApplication 启动类代码如下:
@SpringBootApplication
public class QuestionApplication {
public static void main(String[] args) {
SpringApplication.run(QuestionApplication.class, args);
}
}
提供接口的 HelloWorldController 代码如下:
@RestController
public class HelloWorldController {
@GetMapping("/hi")
public String hello(){
return "hello world";
};
}
目录层级如下图所示,启动能正常访问:http://127.0.0.1:8089/hi
然后调整目录层级机构如下,启动访问 404 错误,问题原因是什么呢?
@SpringBootApplication 注解继承了@ComponentScan注解,此注解默认包扫描为{}, 当为空时,ComponentScanAnnotationParser#parse 方法 扫描的包其实就是QuestionApplication 所在的包,所以,综合来看,这个问题是因为我们不够了解 Spring Boot 的默认扫描规则引起的。
方法一:修改调整包结构
方式二:显式配置 @ComponentScan 或使用 @ComponentScans 来修复问题
ServiceImpl, HelloWorldController 代码如下:
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}
@RestController
public class HelloWorldController {
@Autowired
private ServiceImpl serviceImpl;
@GetMapping("/hi")
public String hello(){
return "hello world" + serviceImpl;
};
}
结果发现,访问多少次http://localhost:8080/hi,访问的结果都是不变的,如下:
hello worldcom.lvt.example.service.ServiceImpl@66d7d9da
很明显,这很可能和我们定义 ServiceImpl 为原型 Bean 的初衷背道而驰,这又是什么原因呢?
当一个属性成员声明为 @Autowired 后,那么在创建 这个 Bean 时,会先使用构造器反射出实例,然后来装配各个标记为 @Autowired 的属性成员。
所以,当一个单例的 Bean,使用 autowired 注解标记其属性时,你一定要注意这个属性值会被固定下来。
方法一:自动注入 Context
修正代码如下:
@RestController
public class HelloWorldController {
@Autowired
private ApplicationContext applicationContext;
@GetMapping("/hi")
public String hello(){
return "hello world" + getServiceImpl();
};
public ServiceImpl getServiceImpl(){
return applicationContext.getBean(ServiceImpl.class);
}
}
ServiceImpl 因为标记为 @Service 而成为一个 Bean。另外我们 ServiceImpl 显式定义了一个构造器。某些编译器现在已能智能提示 构造器参数找不到。
@Service
public class ServiceImpl {
private String serviceName;
public ServiceImpl(String serviceName){
this.serviceName = serviceName;
}
}
但是,上面的代码不是永远都能正确运行的,有时候会报下面这种错误:Parameter 0 of constructor in com.lvt.example.service.ServiceImpl required a bean of type ‘java.lang.String’ that could not be found. 那问题出在原因出在哪里呢?
隐式的规则:我们定义一个类为 Bean,如果再显式定义了构造器,那么这个 Bean 在构建时,会自动根据构造器参数定义寻找对应的 Bean,然后反射创建出这个 Bean。如果存在多个构造器,都可以调用时,到底应该调用哪个呢?最终 Spring 无从选择,只能尝试去调用默认构造器。
方法一:定义一个能让 Spring 装配给 ServiceImpl 构造器参数的 Bean
//这个bean装配给ServiceImpl的构造器参数“serviceName”
@Bean
public String serviceName(){
return "MyServiceName";
}