《Spring编程常见错误50例》读书笔记

源码构建依赖下载可能遇到的问题:

https://blog.csdn.net/xiangxiaotian666/article/details/127399904

1671546630072-cba60336-a06d-47a4-a6df-fbd2d6086c9d.png

Spring Core

  1. 工厂 BeanFactory 内部有 Map
  2. Scan 负责扫描需要实例化的 Bean
  3. 最终使用反射的方式实例化:
    1. java.lang.Class.newInsance();
    2. java.lang.reflect.Constructor.newInstance();
    3. ReflectionFactory.newConstructorForSerialization();
  4. AOP 使用多态代理实现,需要代理哪些类就使用 @Aspect 中定义的 切点 来表示

Bean 定义

隐式扫描不到 Bean 的定义

:::color4
定义的类没有写到 Application 同级或者下级目录的时候就找不到 Bean 定义

:::

解决

可以在 Application 上写上:

  • @ComponentScans(value = { @ComponentScan(value ="com.spring.puzzle.class1.example1.controller") })-
  • @ComponentScan可以多个同时使用,且都生效。效果等同于@ComponentScans
  • @SpringBootApplication(scanBasePackages = {"com.xxx.xxxxx","com.xxx.xxx"})

原理

1
2
3
4
5
6
7
8
9
10
11
12
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
//省略非关键代码
}

Spring 默认规则:ComponentScan 注解的 basePackages 属性指定扫描哪些 Bean 定义。没有指定,默认为空(即{})。此时解析启动类(配置类)的时候,就会为其填充默认值:

画板

定义的 Bean 缺少隐式依赖

:::color4
自定义了构造器,但是没有将构造器的入参注入到 spring,导致 spring 使用对应的构造器实例化 Bean 时候找不到参数而失败

:::

1
2
3
4
5
6
7
@Service
public class ServiceImpl {
private String serviceName;
public ServiceImpl(String serviceName){
this.serviceName = serviceName;
}
}

解决

定义一个Bean,名字就是 serviceName,这个bean装配给ServiceImpl的构造器参数“serviceName”

1
2
3
4
@Bean
public String serviceName(){
return "MyServiceName";
}

原理

我们定义一个类为 Spring Bean A,如果再显式定义了构造器,那么这个 Bean A 在构建时,会自动根据构造器参数定义寻找对应类型的 Bean B,然后反射创建出这个 Bean B 作为 A 构造器的入参去反射创建 Bean A。

画板

自定义多个构造器没有指定优先级

存在两个构造器,都可以调用时,最终 Spring 无从选择,只能尝试去调用默认构造器,而这个默认构造器又不存在,所以测试这个程序它会出错。

1
2
3
4
5
6
7
8
9
10
@Service
public class ServiceImpl {
private String serviceName;
public ServiceImpl(String serviceName){
this.serviceName = serviceName;
}
public ServiceImpl(String serviceName, String otherStringParameter){
this.serviceName = serviceName;
}
}

原型Bean不能如你所愿每次都创建

:::color4
定义了一个原型的 Bean A,在另外一个 Bean B 中使用 @Autowired 注入之后,希望每次调用都是一个新的对象,但是其实是同一个对象。因为 Bean B 其实只实例化了一次,A 作为被依赖方只有在 B 实例化的时候被调用

:::

1
2
3
4
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}
1
2
3
4
5
6
7
8
9
10
11
@RestController
public class HelloWorldController {

@Autowired
private ServiceImpl serviceImpl;

@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi(){
return "helloworld, service is : " + serviceImpl;
};
}

访问多少次,结果都是一样的

解决

不能将 ServiceImpl 的 Bean 固定到属性上的,而应该是每次使用时都会重新获取一次。

方式一:每次从 ApplicationContext 中获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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);
}
}
方式二: 使用 @Lookup
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class HelloWorldController {

@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi(){
return "helloworld, service is : " + getServiceImpl();
};

@Lookup
public ServiceImpl getServiceImpl(){
return null;
}
}

https://blog.csdn.net/qq_25863845/article/details/123475147

标记了 Lookup 的方法最终走入了CGLIB 搞出的类 CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor,这个方法的关键实现参考 LookupOverrideMethodInterceptor#intercept方法调用最终并没有走入案例代码实现的return null语句,而是通过 BeanFactory 来获取 Bean。其实 **getServiceImpl** 方法实现中,随便怎么写都行。

为什么会使用cgLib?是因为在初始化实例的时候(参考 SimpleInstantiationStrategy#instantiate),会发现有方法标记了 Lookup,此时就会添加相应方法到属性methodOverrides 里面去(此过程由 AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors 完成),Lookup 会重写方法。也就是说方法内部的实现,在Bean被实例化的时候就被替换成 CGLIB 的实现了。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
// 当 hasMethodOverrides 为 true 时,则使用 CGLIB。
if (!bd.hasMethodOverrides()) {
//
return BeanUtils.instantiateClass(constructorToUse);
}
else {
// Must generate CGLIB subclass.
return instantiateWithMethodInjection(bd, beanName, owner);
}
}

原理

找到要自动注入的 Bean 后,通过反射设置给对应的field。这个field的执行只发生了一次,所以后续就固定起来了,并不会因为 ServiceImpl 标记了 SCOPE_PROTOTYPE 而改变。所以我们请求多少次都不会重新去Spring容器获取 ServiceImpl,而是直接获取 Controller 引用的那个对象。

画板

注入

required a single bean, but 2 were found

:::danger
@Autowired 的一个接口有多个实现的时候,就无法工作了

:::

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@Slf4j
@Validated
public class StudentController {
@Autowired
DataService dataService;

@RequestMapping(path = "students/{id}", method = RequestMethod.DELETE)
public void deleteStudent(@PathVariable("id") @Range(min = 1,max = 100) int id){
dataService.deleteStudent(id);
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface DataService {
void deleteStudent(int id);
}

@Repository
@Slf4j
public class OracleDataService implements DataService{
@Override
public void deleteStudent(int id) {
log.info("delete student info maintained by oracle");
}
}

@Repository
@Slf4j
public class CassandraDataService implements DataService{
@Override
public void deleteStudent(int id) {
log.info("delete student info maintained by cassandra");
}
}

解决

让候选实现类具有优先级或压根可以不去选择。

方式一:设置优先级
1
2
3
4
5
6
@Repository
@Primary
@Slf4j
public class OracleDataService implements DataService{
//省略非关键代码
}
方式二:指定精确 Bean名字

将属性名和Bean名字精确匹配,这样就可以让注入选择不犯难

1
2
@Autowired
DataService oracleDataService;
方式三:使用 @Qualifier 显式指定使用哪种
1
2
3
@Autowired()
@Qualifier("cassandraDataService")
DataService dataService;
方式四:使用策略模式存到自己定义的 map 中

自己定义一个策略类,实现 InitializingBean