|
|
|
|
# Java 高级
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 0. 目录
|
|
|
|
|
|
|
|
|
|
Java并发编程
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Spring 常见错误笔记
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 2. Spring 常见错误笔记
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 2.1 Spring Core - SpringBean 定义常见错误
|
|
|
|
|
|
|
|
|
|
#### A. 案例 1:隐式扫描不到 Bean 的定义
|
|
|
|
|
- 因为包结构的定义, 造成相关的定义类扫描出现错误
|
|
|
|
|
- **案例解析**
|
|
|
|
|
- SpringBootApplication 开启了很多功能,其中一个关键功能就是 ComponentScan 注解的 basePackages 属性指定的扫描包的路径
|
|
|
|
|
- **问题修复**
|
|
|
|
|
- 方法1: 检查是否可以手动调节至默认的扫包路径下, ComponentScanAnnotationParser#parse 方法, declaringClass(XxxApplication) 所在的包的路径
|
|
|
|
|
- 方法2: 显式配置 @ComponentScans 来修复问题, 确保所有的需要被扫描到的包都被扫描到
|
|
|
|
|
- PS: 注意仅仅使用 @ComponentScan 一旦显式指定其它包,原来地默认扫描包就被忽略了.
|
|
|
|
|
|
|
|
|
|
#### B. 案例 2:定义的 Bean 缺少隐式依赖
|
|
|
|
|
- 定义 Spring Bean
|
|
|
|
|
```java
|
|
|
|
|
|
|
|
|
|
@Service // ServiceImpl 因为标记为 @Service 而成为一个 Bean
|
|
|
|
|
public class ServiceImpl {
|
|
|
|
|
private String serviceName;
|
|
|
|
|
public ServiceImpl(String serviceName){ // ServiceImpl 显式定义了一个构造器
|
|
|
|
|
this.serviceName = serviceName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
> 代码报错:
|
|
|
|
|
> 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 时,调用的方法是 AbstractAutowireCapableBeanFactory#createBeanInstance
|
|
|
|
|
- 它主要包含两大基本步骤:寻找构造器和通过反射调用构造器创建实例
|
|
|
|
|
```java
|
|
|
|
|
// Candidate constructors for autowiring?
|
|
|
|
|
// Spring 会先执行 determineConstructorsFromBeanPostProcessors 方法来获取构造器
|
|
|
|
|
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
|
|
|
|
|
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
|
|
|
|
|
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
|
|
|
|
|
// 然后通过 autowireConstructor 方法带着构造器去创建实例
|
|
|
|
|
// autowireConstructor 方法要创建实例,不仅需要知道是哪个构造器,还需要知道构造器对应的参数
|
|
|
|
|
return autowireConstructor(beanName, mbd, ctors, args);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 参考如下(即 ConstructorResolver#instantiate):
|
|
|
|
|
```java
|
|
|
|
|
private Object instantiate(
|
|
|
|
|
String beanName, RootBeanDefinition mbd, Constructor<?> constructorToUse, Object[] argsToUse) {
|
|
|
|
|
```
|
|
|
|
|
- 上述方法中存储构造参数的 argsToUse 如何获取呢?
|
|
|
|
|
- 当我们已经知道构造器 ServiceImpl(String serviceName),要创建出 ServiceImpl 实例,如何确定 serviceName 的值是多少?
|
|
|
|
|
- 不能直接显式使用 new 关键字来创建实例
|
|
|
|
|
- Spring 只能是去寻找依赖来作为构造器调用参数
|
|
|
|
|
- 参数如何获取? ConstructorResolver#autowireConstructor
|
|
|
|
|
```java
|
|
|
|
|
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
|
|
|
|
|
getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
|
|
|
|
|
```
|
|
|
|
|
- 调用 createArgumentArray 方法来构建调用构造器的参数数组,而这个方法的最终实现是从 BeanFactory 中获取 Bean,
|
|
|
|
|
org.springframework.beans.factory.support.ConstructorResolver#resolveAutowiredArgument
|
|
|
|
|
```java
|
|
|
|
|
try {
|
|
|
|
|
return this.beanFactory.resolveDependency(
|
|
|
|
|
new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 上述的调用即是根据参数来寻找对应的 Bean
|
|
|
|
|
- **问题修正**
|
|
|
|
|
- 因为不了解很多隐式的规则:我们定义一个类为 Bean,如果再显式定义了构造器,那么这个 Bean 在构建时,会自动根据构造器参数定义寻找对应的 Bean,然后反射创建出这个 Bean.
|
|
|
|
|
- 了解了这个隐式规则后,解决这个问题就简单多了。我们可以直接定义一个能让 Spring 装配给 ServiceImpl 构造器参数的 Bean
|
|
|
|
|
```java
|
|
|
|
|
//这个bean装配给ServiceImpl的构造器参数“serviceName”
|
|
|
|
|
@Bean
|
|
|
|
|
public String serviceName(){
|
|
|
|
|
return "MyServiceName";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
- 再次运行程序,发现一切正常了。
|
|
|
|
|
- 我们在使用 Spring 时,不要总想着定义的 Bean 也可以在非 Spring 场合直接用 new 关键字显式使用,这种思路是不可取的
|
|
|
|
|
- 类似的,假设我们不了解 Spring 的隐式规则,在修正问题后,我们可能写出更多看似可以运行的程序
|
|
|
|
|
```java
|
|
|
|
|
@Service
|
|
|
|
|
public class ServiceImpl {
|
|
|
|
|
private String serviceName;
|
|
|
|
|
public ServiceImpl(String serviceName){
|
|
|
|
|
this.serviceName = serviceName;
|
|
|
|
|
}
|
|
|
|
|
public ServiceImpl(String serviceName, String otherStringParameter){
|
|
|
|
|
this.serviceName = serviceName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 如果我们仍用非 Spring 的思维去审阅这段代码,可能不会觉得有什么问题,毕竟 String 类型可以自动装配了,无非就是增加了一个 String 类型的参数而已。
|
|
|
|
|
- 但是如果你了解 Spring 内部是用反射来构建 Bean 的话,就不难发现问题所在:存在两个构造器,都可以调用时,到底应该调用哪个呢?最终 Spring 无从选择,只能尝试去调用默
|
|
|
|
|
认构造器,而这个默认构造器又不存在,所以测试这个程序它会出错。
|
|
|
|
|
- PS: 构造器的参数添加@Autowired(required = false)
|
|
|
|
|
- PS: @Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE)这个是说在每次注入的时候回自动创建一个新的bean实例
|
|
|
|
|
@Scope(value=ConfigurableBeanFactory.SCOPE_SINGLETON)单例模式,在整个应用中只能创建一个实例。
|
|
|
|
|
- PS: @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
#### C. 案例 3:原型 Bean 被固定
|
|
|
|
|
- 在定义 Bean 时,有时候我们会使用原型 Bean,例如定义如下:
|
|
|
|
|
```java
|
|
|
|
|
@Service
|
|
|
|
|
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
|
|
|
|
public class ServiceImpl {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 然后我们按照下面的方式去使用它:
|
|
|
|
|
```java
|
|
|
|
|
@RestController
|
|
|
|
|
public class HelloWorldController {
|
|
|
|
|
@Autowired
|
|
|
|
|
private ServiceImpl serviceImpl;
|
|
|
|
|
@RequestMapping(path = "hi", method = RequestMethod.GET)
|
|
|
|
|
public String hi(){
|
|
|
|
|
return "helloworld, service is : " + serviceImpl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 结果,我们会发现,不管我们访问多少次http://localhost:8080/hi,访问的结果都是不变的
|
|
|
|
|
> helloworld, service is : com.spring.puzzle.class1.example3.error.ServiceImpl@4908af
|
|
|
|
|
- 很明显,这很可能和我们定义 ServiceImpl 为原型 Bean 的初衷背道而驰
|
|
|
|
|
- **案例解析**
|
|
|
|
|
- 当一个属性成员 serviceImpl 声明为 @Autowired 后,那么在创建 HelloWorldController 这个 Bean 时,会先使用构造器反射出实例,然后来装配各个标记
|
|
|
|
|
为 @Autowired 的属性成员(装配方法参考 AbstractAutowireCapableBeanFactory#populateBean)
|
|
|
|
|
- 具体到执行过程,它会使用很多 BeanPostProcessor 来做完成工作,其中一种是 AutowiredAnnotationBeanPostProcessor,
|
|
|
|
|
它会通过 DefaultListableBeanFactory#findAutowireCandidates 寻找到 ServiceImpl 类型的 Bean,然后设置给对应的属性(即 serviceImpl 成员)
|
|
|
|
|
- 关键执行步骤可参考 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject:
|
|
|
|
|
```java
|
|
|
|
|
@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) {
|
|
|
|
|
value = resolvedCachedArgument(beanName, this.cachedFieldValue);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// ...
|
|
|
|
|
try {
|
|
|
|
|
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
|
|
|
|
|
}
|
|
|
|
|
catch (BeansException ex) {
|
|
|
|
|
throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
|
|
|
|
|
}
|
|
|
|
|
// ...
|
|
|
|
|
}
|
|
|
|
|
if (value != null) {
|
|
|
|
|
//将bean设置给成员字段
|
|
|
|
|
ReflectionUtils.makeAccessible(field);
|
|
|
|
|
field.set(bean, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 待我们寻找到要自动注入的 Bean 后,即可通过反射设置给对应的 field.这个 field 的执行只发生了一次,所以后续就固定起来了,这个 field 的执
|
|
|
|
|
行只发生了一次,所以后续就固定起来了
|
|
|
|
|
- 所以,当一个单例的 Bean,使用 autowired 注解标记其属性时,你一定要注意这个属性值会被固定下来。
|
|
|
|
|
- **问题修正**
|
|
|
|
|
- 我们可以知道要修正这个问题,肯定是不能将 ServiceImpl 的 Bean 固定到属性上的,而应该是每次使用时都会重新获取一次。
|
|
|
|
|
- **修复方法1**: 自动注入 Context
|
|
|
|
|
- 即自动注入 ApplicationContext,然后定义 getServiceImpl() 方法,在方法中获取一个新的 ServiceImpl 类型实例。修正代码如下:
|
|
|
|
|
```java
|
|
|
|
|
@RestController
|
|
|
|
|
public class HelloWorldController {
|
|
|
|
|
@Autowired
|
|
|
|
|
private ApplicationContext applicationContext;
|
|
|
|
|
|
|
|
|
|
@RequestMapping(path = "hi", method = RequestMethod.GET)
|
|
|
|
|
public String hi(){
|
|
|
|
|
return "helloworld, service is : " + getServiceImpl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ServiceImpl getServiceImpl(){
|
|
|
|
|
return applicationContext.getBean(ServiceImpl.class);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- **修复方法2**: 使用 Lookup 注解
|
|
|
|
|
- 类似修正方法 1,也添加一个 getServiceImpl 方法,不过这个方法是被 Lookup 标记的。
|
|
|
|
|
- 修正代码如下:
|
|
|
|
|
```java
|
|
|
|
|
@RestController
|
|
|
|
|
public class HelloWorldController {
|
|
|
|
|
|
|
|
|
|
@RequestMapping(path = "hi", method = RequestMethod.GET)
|
|
|
|
|
public String hi(){
|
|
|
|
|
return "helloworld, service is : " + getServiceImpl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Lookup
|
|
|
|
|
public ServiceImpl getServiceImpl(){
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 通过这两种修正方式,再次测试程序,我们会发现结果已经符合预期(每次访问这个接口,都会创建新的 Bean)。
|
|
|
|
|
- 讨论下 Lookup 是如何生效的。毕竟在修正代码中,我们看到 getServiceImpl 方法的实现返回值是 null,这或许很难说服自己。
|
|
|
|
|
- 通过 Debug 我们最终的执行因为标记了 Lookup 而走入了 CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor,这个方
|
|
|
|
|
法的关键实现参考 LookupOverrideMethodInterceptor#intercept:
|
|
|
|
|
```java
|
|
|
|
|
@Override
|
|
|
|
|
public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
|
|
|
|
|
// Cast is safe, as CallbackFilter filters are used selectively.
|
|
|
|
|
LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
|
|
|
|
|
Assert.state(lo != null, "LookupOverride not found");
|
|
|
|
|
Object[] argsToUse = (args.length > 0 ? args : null); // if no-arg, don't insist on args at all
|
|
|
|
|
if (StringUtils.hasText(lo.getBeanName())) {
|
|
|
|
|
return (argsToUse != null ? this.owner.getBean(lo.getBeanName(), argsToUse) :
|
|
|
|
|
this.owner.getBean(lo.getBeanName()));
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
return (argsToUse != null ? this.owner.getBean(method.getReturnType(), argsToUse) :
|
|
|
|
|
this.owner.getBean(method.getReturnType()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 我们的方法调用最终并没有走入案例代码实现的 return null 语句,而是通过 BeanFactory 来获取 Bean。
|
|
|
|
|
- 其实在我们的 getServiceImpl 方法实现中,随便怎么写都行,这不太重要。
|
|
|
|
|
- 为什么我们走入了 CGLIB 搞出的类,这是因为我们有方法标记了Lookup。我们可以从下面的这段代码得到验证,参考 SimpleInstantiationStrategy#instantiate:
|
|
|
|
|
```java
|
|
|
|
|
@Override
|
|
|
|
|
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
|
|
|
|
|
// Don't override the class with CGLIB if no overrides.
|
|
|
|
|
// 当 hasMethodOverrides 为 true 时,则使用 CGLIB
|
|
|
|
|
if (!bd.hasMethodOverrides()) {
|
|
|
|
|
// ...
|
|
|
|
|
return BeanUtils.instantiateClass(constructorToUse);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// Must generate CGLIB subclass.
|
|
|
|
|
return instantiateWithMethodInjection(bd, beanName, owner);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 条件的成立在于解析 HelloWorldController 这个 Bean 时,我们会发现有方法标记了 Lookup,此时就会添加相应方法到属性 methodOverrides 里面去
|
|
|
|
|
(此过程由AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors 完成)
|
|
|
|
|
|
|
|
|
|
### 2.1 Spring Core - Spring Bean 依赖注入常见错误
|
|
|
|
|
- Spring @Autowired “控制反转、依赖注入”
|
|
|
|
|
#### A. 案例 1:过多赠予,无所适从
|
|
|
|
|
- @Autowired 时,不管你是菜鸟级还是专家级的 Spring 使用者,都应该制造或者遭遇过类似的错误:
|
|
|
|
|
> required a single bean, but 2 were found
|
|
|
|
|
- 我们仅需要一个 Bean,但实际却提供了 2 个(这里的“2”在实际错误中可能是其它大于 1 的任何数字)
|
|
|
|
|
- **案例解析**
|
|
|
|
|
- 要找到这个问题的根源,我们就需要对 @Autowired 实现的依赖注入的原理有一定地了解。首先,我们先来了解下 @Autowired 发生的位置和核心过程。
|
|
|
|
|
- 首先,我们先来了解下 @Autowired 发生的位置和核心过程。
|
|
|
|
|
- 当一个 Bean 被构建时,核心包括两个基本步骤:
|
|
|
|
|
- 执行 AbstractAutowireCapableBeanFactory#createBeanInstance 方法:通过构造器反射构造出这个 Bean,在相当于构建出 业务Controller 的实例;
|
|
|
|
|
- 执行 AbstractAutowireCapableBeanFactory#populate 方法:填充(即设置)这个Bean,相当于设置 Controller 实例中被 @Autowired 标记的 Service 属性成员
|
|
|
|
|
- 在步骤 2 中,“填充”过程的关键就是执行各种 BeanPostProcessor 处理器,关键代码:
|
|
|
|
|
```java
|
|
|
|
|
@SuppressWarnings("deprecation") // for postProcessPropertyValues
|
|
|
|
|
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
|
|
|
|
|
// ...
|
|
|
|
|
if (hasInstAwareBpps) {
|
|
|
|
|
if (pvs == null) {
|
|
|
|
|
pvs = mbd.getPropertyValues();
|
|
|
|
|
}
|
|
|
|
|
for (BeanPostProcessor bp : getBeanPostProcessors()) {
|
|
|
|
|
if (bp instanceof InstantiationAwareBeanPostProcessor) {
|
|
|
|
|
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
|
|
|
|
|
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
|
|
|
|
|
// ...
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// ...
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 因为 Controller 含有标记为 Autowired 的成员属性 Service,所以会使用到 AutowiredAnnotationBeanPostProcessor(BeanPostProcessor 中的一种)来完
|
|
|
|
|
成“装配”过程:找出合适的 Service 的 bean 并设置给 Controller#Service。如果深究这个装配过程,又可以细分为两个步骤:
|
|
|
|
|
- 寻找出所有需要依赖注入的字段和方法,参考 AutowiredAnnotationBeanPostProcessor#postProcessProperties 中的代码行:
|
|
|
|
|
- ```java
|
|
|
|
|
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
|
|
|
|
|
```
|
|
|
|
|
- 根据依赖信息寻找出依赖并完成注入,以字段注入为例,参考 AutowiredFieldElement#inject 方法:
|
|
|
|
|
- ```java
|
|
|
|
|
@Override
|
|
|
|
|
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
|
|
|
|
|
Field field = (Field) this.member;
|
|
|
|
|
Object value;
|
|
|
|
|
if (this.cached) {
|
|
|
|
|
value = resolvedCachedArgument(beanName, this.cachedFieldValue);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// ...
|
|
|
|
|
try {
|
|
|
|
|
// 寻找“依赖”,desc为"自定义Service"的DependencyDescriptor
|
|
|
|
|
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
|
|
|
|
|
}
|
|
|
|
|
// ...
|
|
|
|
|
if (value != null) {
|
|
|
|
|
ReflectionUtils.makeAccessible(field);
|
|
|
|
|
// //装配“依赖”
|
|
|
|
|
field.set(bean, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- DefaultListableBeanFactory#doResolveDependency 中代码片段可以查看实际装配了什么依赖
|
|
|
|
|
- 如果同时满足以下两个条件则会抛出本案例的错误:
|
|
|
|
|
- 调用 determineAutowireCandidate 方法来选出优先级最高的依赖,但是发现并没有优先级可依据。具体选择过程可参考 DefaultListableBeanFactory#determineAutowireCandidate:
|
|
|
|
|
```java
|
|
|
|
|
@Nullable
|
|
|
|
|
protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {
|
|
|
|
|
Class<?> requiredType = descriptor.getDependencyType();
|
|
|
|
|
// @Primary
|
|
|
|
|
String primaryCandidate = determinePrimaryCandidate(candidates, requiredType);
|
|
|
|
|
if (primaryCandidate != null) {
|
|
|
|
|
return primaryCandidate;
|
|
|
|
|
}
|
|
|
|
|
String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType);
|
|
|
|
|
if (priorityCandidate != null) {
|
|
|
|
|
return priorityCandidate;
|
|
|
|
|
}
|
|
|
|
|
// Fallback
|
|
|
|
|
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
|
|
|
|
|
String candidateName = entry.getKey();
|
|
|
|
|
Object beanInstance = entry.getValue();
|
|
|
|
|
if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||
|
|
|
|
|
matchesBeanName(candidateName, descriptor.getDependencyName())) {
|
|
|
|
|
return candidateName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 如代码所示,优先级的决策是先根据 @Primary 来决策,其次是 @Priority 决策, 最后是根据 Bean 名字的严格匹配来决策。
|
|
|
|
|
- 如果这些帮助决策优先级的注解都没有被使用,名字也不精确匹配,则返回 null,告知无法决策出哪种最合适。
|
|
|
|
|
- @Autowired 要求是必须注入的(即 required 保持默认值为 true),或者注解的属性类型并不是可以接受多个 Bean 的类型,例如数组、Map、集合。这点可以参考
|
|
|
|
|
DefaultListableBeanFactory#indicatesMultipleBeans 的实现:
|
|
|
|
|
```java
|
|
|
|
|
private boolean indicatesMultipleBeans(Class<?> type) {
|
|
|
|
|
return (type.isArray() || (type.isInterface() &&
|
|
|
|
|
(Collection.class.isAssignableFrom(type) || Map.class.isAssignableFro
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 如果我们把这些条件想得简单点,或许更容易帮助我们去理解这个设计.就像我们遭遇多个无法比较优劣的选择,却必须选择其一时,与其偷偷地随便选择一种,还不如直接报错,起码可以避免更严重的问题发生。
|
|
|
|
|
- **问题修正**
|
|
|
|
|
- 找到解决问题的方法:打破上述两个条件中的任何一个即可,即让候选项具有优先级或压根可以不去选择
|
|
|
|
|
- 不过需要你注意的是,不是每一种条件的打破都满足实际需求,例如我们可以通过使用标记 @Primary 的方式来让被标记的候选者有更高优先级,从而避免报错,但是它并不一定符合业务需求,这就好
|
|
|
|
|
比我们本身需要两种数据库都能使用,而不是顾此失彼。
|
|
|
|
|
```java
|
|
|
|
|
@Repository
|
|
|
|
|
@Primary
|
|
|
|
|
@Slf4j
|
|
|
|
|
public class OracleDataService implements DataService{
|
|
|
|
|
//省略非关键代码
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 现在,请你仔细研读上述的两个条件,要同时支持多种 DataService,且能在不同业务情景下精确匹配到要选择到的 DataService,我们可以使用下面的方式去修改:
|
|
|
|
|
```java
|
|
|
|
|
@Autowired
|
|
|
|
|
DataService oracleDataService;
|
|
|
|
|
````
|
|
|
|
|
- 如代码所示,修改方式的精髓在于将属性名和 Bean 名字精确匹配,这样就可以让注入选择不犯难:需要 Oracle 时指定属性名为 oracleDataService,需要 Cassandra 时则指定属性名为 cassandraDataService
|
|
|
|
|
- PS 这里可以使用注解进行统一使用什么数据库的配置, 也就是自动进行注入使用什么依赖
|
|
|
|
|
|
|
|
|
|
#### B. 显式引用 Bean 时首字母忽略大小写
|
|
|
|
|
- 针对案例 1 的问题修正,实际上还存在另外一种常用的解决办法,即采用 @Qualifier 来显式指定引用的是那种服务,例如采用下面的方式:
|
|
|
|
|
```java
|
|
|
|
|
@Autowired()
|
|
|
|
|
@Qualifier("cassandraDataService")
|
|
|
|
|
DataService dataService;
|
|
|
|
|
```
|
|
|
|
|
这种方式之所以能解决问题,在于它能让寻找出的 Bean 只有一个(即精确匹配),所以压根不会出现后面的决策过程,可以参考 DefaultListableBeanFactory#doResolveDependency:
|
|
|
|
|
```java
|
|
|
|
|
@Nullable
|
|
|
|
|
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
|
|
|
|
|
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
|
|
|
|
|
|
|
|
|
|
// ...
|
|
|
|
|
// 寻找 Bean 的过程
|
|
|
|
|
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
|
|
|
|
|
if (matchingBeans.isEmpty()) {
|
|
|
|
|
if (isRequired(descriptor)) {
|
|
|
|
|
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String autowiredBeanName;
|
|
|
|
|
Object instanceCandidate;
|
|
|
|
|
|
|
|
|
|
if (matchingBeans.size() > 1) {
|
|
|
|
|
// 省略多个bean的决策过程
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// We have exactly one match.
|
|
|
|
|
Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
|
|
|
|
|
autowiredBeanName = entry.getKey();
|
|
|
|
|
instanceCandidate = entry.getValue();
|
|
|
|
|
}
|
|
|
|
|
// ...
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 我们会使用 @Qualifier 指定的名称去匹配,最终只找到了唯一一个。[这个可以作为定义Bean的一种规范写在程序中, 最好是定义在一个类文件中,这样就避免了装配问题的发生]
|
|
|
|
|
- 不过在使用 @Qualifier 时,我们有时候会犯另一个经典的小错误,就是我们可能会忽略 Bean 的名称首字母大小写。这里我们把校正后的案例稍稍变形如下:
|
|
|
|
|
```java
|
|
|
|
|
@Autowired
|
|
|
|
|
@Qualifier("CassandraDataService")
|
|
|
|
|
DataService dataService;
|
|
|
|
|
```
|
|
|
|
|
- 这样会继续报错
|
|
|
|
|
- 因为: 对于 Bean 的名字,如果没有显式指明,就应该是类名,不过首字母应该小写。
|
|
|
|
|
- PS: 可以配合 Bean的 value和name配置别名,但是两个属性不能共存,也不能使用特殊符号,也不能同时配置多个别名
|
|
|
|
|
|
|
|
|
|
- **案例解析**
|
|
|
|
|
- 当因为名称问题(例如引用 Bean 首字母搞错了)找不到 Bean 时,会直接抛出 NoSuchBeanDefinitionException。
|
|
|
|
|
- 在这里,我们真正需要关心的问题是:不显式设置名字的 Bean,其默认名称首字母到底是大写还是小写呢?
|
|
|
|
|
- 当我们启动基于 Spring Boot 的应用程序时,会自动扫描我们的Package,以找出直接或间接标记了 @Component 的 Bean 的定义(即BeanDefinition)。
|
|
|
|
|
例如 CassandraDataService、SQLiteDataService 都被标记了 @Repository,而 Repository 本身被 @Component 标记,所以它们都是间接标记了
|
|
|
|
|
@Component。
|
|
|
|
|
- 一旦找出这些 Bean 的信息,就可以生成这些 Bean 的名字,然后组合成一个个BeanDefinitionHolder 返回给上层。这个过程关键步骤可以查看下图的代码片段
|
|
|
|
|
(ClassPathBeanDefinitionScanner#doScan):
|
|
|
|
|
- ![显式引用Bean时首字母忽略大小写](pic/显式引用Bean时首字母忽略大小写.png)
|
|
|
|
|
- 基本匹配我们前面描述的过程,其中方法调用BeanNameGenerator#generateBeanName 即用来产生 Bean 的名字,它有两种实现方式。因为 DataService 的实现都是使用注解标记的,
|
|
|
|
|
式。因为 DataService 的实现都是使用注解标记的,的其实是 AnnotationBeanNameGenerator#generateBeanName 这种实现方式,我们可以看下它的具体实现,代码如下:
|
|
|
|
|
```java
|
|
|
|
|
@Override
|
|
|
|
|
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
|
|
|
|
|
if (definition instanceof AnnotatedBeanDefinition) {
|
|
|
|
|
String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
|
|
|
|
|
if (StringUtils.hasText(beanName)) {
|
|
|
|
|
// Explicit bean name found.
|
|
|
|
|
return beanName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Fallback: generate a unique default bean name.
|
|
|
|
|
return buildDefaultBeanName(definition, registry);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 大体流程只有两步:看 Bean 有没有显式指明名称,如果有则用显式名称,如果没有则产生一个默认名称。
|
|
|
|
|
- 很明显,在我们的案例中,是没有给 Bean 指定名字的,所以产生的 Bean 的名称就是生成的默认名称,查看默认名的产生方法 buildDefaultBeanName,其
|
|
|
|
|
实现如下:
|
|
|
|
|
```java
|
|
|
|
|
protected String buildDefaultBeanName(BeanDefinition definition) {
|
|
|
|
|
String beanClassName = definition.getBeanClassName();
|
|
|
|
|
Assert.state(beanClassName != null, "No bean class name set");
|
|
|
|
|
String shortClassName = ClassUtils.getShortName(beanClassName);
|
|
|
|
|
return Introspector.decapitalize(shortClassName);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 首先,获取一个简短的 ClassName,然后调用 Introspector#decapitalize 方法,设置首字母大写或小写,具体参考下面的代码实现:
|
|
|
|
|
```java
|
|
|
|
|
public static String decapitalize(String name) {
|
|
|
|
|
if (name == null || name.length() == 0) {
|
|
|
|
|
return name;
|
|
|
|
|
}
|
|
|
|
|
if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
|
|
|
|
|
Character.isUpperCase(name.charAt(0))){
|
|
|
|
|
return name;
|
|
|
|
|
}
|
|
|
|
|
char chars[] = name.toCharArray();
|
|
|
|
|
chars[0] = Character.toLowerCase(chars[0]);
|
|
|
|
|
return new String(chars);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 到这,我们很轻松地明白了前面两个问题出现的原因:**如果一个类名是以两个大写字母开头的,则首字母不变,其它情况下默认首字母变成小写。**
|
|
|
|
|
- 结合我们之前的案例,SQLiteDataService 的 Bean,其名称应该就是类名本身,而 CassandraDataService 的Bean 名称则变成了首字母小写(cassandraDataService)
|
|
|
|
|
- **问题修正**
|
|
|
|
|
- 方法1 : 引用处纠正首字母大小写问题:
|
|
|
|
|
```java
|
|
|
|
|
@Autowired
|
|
|
|
|
@Qualifier("cassandraDataService")
|
|
|
|
|
DataService dataService;
|
|
|
|
|
```
|
|
|
|
|
- 方法2 : 定义处显式指定 Bean 名字,我们可以保持引用代码不变,而通过显式指明CassandraDataService 的 Bean 名称为 CassandraDataService 来纠正这个问题
|
|
|
|
|
```java
|
|
|
|
|
@Repository("CassandraDataService")
|
|
|
|
|
@Slf4j
|
|
|
|
|
public class CassandraDataService implements DataService {
|
|
|
|
|
//省略实现
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 我们的程序就可以精确匹配到要找的 Bean 了。比较一下这两种修改方法的话,如果你不太了解源码,不想纠结于首字母到底是大写还是小写,建议你用第二种方法去避免困扰。
|
|
|
|
|
|
|
|
|
|
#### C. 引用内部类的 Bean 遗忘类名
|
|
|
|
|
- 我们需要定义一个内部类来实现一种新的 DataService,代码如下:
|
|
|
|
|
```java
|
|
|
|
|
public class StudentController {
|
|
|
|
|
@Repository
|
|
|
|
|
public static class InnerClassDataService implements DataService{
|
|
|
|
|
@Override
|
|
|
|
|
public void deleteStudent(int id) {
|
|
|
|
|
//空实现
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//省略其他非关键代码
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
- 遇到这种情况,我们一般都会很自然地用下面的方式直接去显式引用这个 Bean:
|
|
|
|
|
```java
|
|
|
|
|
@Autowired
|
|
|
|
|
@Qualifier("innerClassDataService")
|
|
|
|
|
DataService innerClassDataService;
|
|
|
|
|
```
|
|
|
|
|
- 很明显,有了案例 2 的经验,我们上来就直接采用了首字母小写以避免案例 2 中的错误 但这样的代码是不是就没问题了呢?实际上,仍然会报错“找不到 Bean”,这是为什么?
|
|
|
|
|
|
|
|
|
|
- **案例解析**
|
|
|
|
|
- 实际上,我们遭遇的情况是“如何引用内部类的 Bean”。解析案例 2 的时候,我曾经贴出了如何产生默认 Bean 名的方法(即AnnotationBeanNameGenerator#buildDefaultBeanName)
|
|
|
|
|
当时我们只关注了首字母是否小写的代码片段,而在最后变换首字母之前,有一行语句是对 class 名字的处理,代码如下:
|
|
|
|
|
```java
|
|
|
|
|
String shortClassName = ClassUtils.getShortName(beanClassName);
|
|
|
|
|
```
|
|
|
|
|
- 我们可以看下它的实现,参考 ClassUtils#getShortName 方法:
|
|
|
|
|
```java
|
|
|
|
|
public static String getShortName(String className) {
|
|
|
|
|
Assert.hasLength(className, "Class name must not be empty");
|
|
|
|
|
int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR);
|
|
|
|
|
int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR);
|
|
|
|
|
if (nameEndIndex == -1) {
|
|
|
|
|
nameEndIndex = className.length();
|
|
|
|
|
}
|
|
|
|
|
String shortName = className.substring(lastDotIndex + 1, nameEndIndex);
|
|
|
|
|
shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR);
|
|
|
|
|
return shortName;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
- 很明显,假设我们是一个内部类,例如下面的类名:
|
|
|
|
|
- com.spring.puzzle.class2.example3.StudentController.InnerClassDataService
|
|
|
|
|
- 在经过这个方法的处理后,我们得到的其实是下面这个名称:
|
|
|
|
|
- StudentController.InnerClassDataService
|
|
|
|
|
- 最后经过 Introspector.decapitalize 的首字母变换,最终获取的 Bean 名称如下:
|
|
|
|
|
- studentController.InnerClassDataService
|
|
|
|
|
- 所以我们在案例程序中,直接使用 innerClassDataService 自然找不到想要的 Bean
|
|
|
|
|
- **问题修正**
|
|
|
|
|
- 上面源码的跟踪结果显示, 通过下面的方法进行修复
|
|
|
|
|
```java
|
|
|
|
|
@Autowired
|
|
|
|
|
@Qualifier("studentController.InnerClassDataService")
|
|
|
|
|
DataService innerClassDataService;
|
|
|
|
|
```
|
|
|
|
|
- 这个引用看起来有些许奇怪,但实际上是可以工作的,反而直接使用innerClassDataService 来引用倒是真的不可行。
|
|
|
|
|
- **对源码的学习是否全面决定了我们以后犯错的可能性大小**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|