You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

566 lines
28 KiB
Markdown

# 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所以会使用到 AutowiredAnnotationBeanPostProcessorBeanPostProcessor 中的一种)来完
成“装配”过程:找出合适的 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 来引用倒是真的不可行。
- **对源码的学习是否全面决定了我们以后犯错的可能性大小**