diff --git a/code-language/java/README.md b/code-language/java/README.md index 8f8661e..182c362 100644 --- a/code-language/java/README.md +++ b/code-language/java/README.md @@ -431,6 +431,130 @@ DataService dataService; @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 来引用倒是真的不可行。 +- **对源码的学习是否全面决定了我们以后犯错的可能性大小** + + + diff --git a/code-language/java/pic/显式引用Bean时首字母忽略大小写.png b/code-language/java/pic/显式引用Bean时首字母忽略大小写.png new file mode 100644 index 0000000..7e7153b Binary files /dev/null and b/code-language/java/pic/显式引用Bean时首字母忽略大小写.png differ