diff --git a/code-language/java/README.md b/code-language/java/README.md new file mode 100644 index 0000000..182c362 --- /dev/null +++ b/code-language/java/README.md @@ -0,0 +1,565 @@ +# 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 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 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 autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { + + // ... + // 寻找 Bean 的过程 + Map 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 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 来引用倒是真的不可行。 +- **对源码的学习是否全面决定了我们以后犯错的可能性大小** + + + + + + + + + + + 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