【源码专题】Spring源码深入剖析之ApplicationContext解决多实现类依赖注入问题
本文将从解决多实现类依赖注入问题,深入剖析Spring源码之ApplicationContext。
问题
一般一个接口只有一个实现类,但是在接口有多个使用场景的情况下,一个接口可以有多个实现类。
这时候我们直接使用@Autowired注解进行依赖注入,会出现依赖注入错误,如下所示:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
快速实践
这里我们来复现上面提到的问题,并分别采用不同的解决方案进行实践:
1. 添加依赖
创建一个springboot的项目,然后添加如下web依赖到项目中即可:
<!-- web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. 创建Service接口
创建service目录,然后在目录下添加TestService接口:
package com.deepinsea.springbootdemo.service;
/**
* Created by deepinsea on 2022/8/22.
*/
public interface TestService {
String test();
}
3. 创建Service实现类
在service目录下创建impl文件夹,然后分别创建两个实现类TestAServiceImpl和TestBServiceImpl:
TestAServiceImpl
package com.deepinsea.springbootdemo.service.impl;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.stereotype.Service;
/**
* Created by deepinsea on 2022/8/22.
*/
@Service
public class TestAServiceImpl implements TestService {
@Override
public String test() {
return "A";
}
}
TestBServiceImpl
package com.deepinsea.springbootdemo.service.impl;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.stereotype.Service;
/**
* Created by deepinsea on 2022/8/22.
*/
@Service
public class TestBServiceImpl implements TestService {
@Override
public String test() {
return "B";
}
}
4. 创建Controller类
创建controller目录,然后在目录下创建TestController类:
package com.deepinsea.springbootdemo.controller;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by deepinsea on 2022/8/22.
* 解决一个Service多个实现类,Bean依赖冲突的问题
*/
@RestController
public class TestController {
// 直接使用@Autowired注入service接口,出现bean冲突的问题
@Autowired
private TestService testService;
@GetMapping("")
public void test(){
String t1 = testService.test();
System.out.println(t1);
}
}
启动项目,不出意外的出现了Bean依赖注入的问题:
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-08-29 00:45:13.382 ERROR 41516 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Field testService in com.deepinsea.springbootdemo.controller.TestController required a single bean, but 2 were found:
- testAServiceImpl: defined in file [E:\IdeaProjects\DemoProjects\springboot-demo\target\classes\com\deepinsea\springbootdemo\service\impl\TestAServiceImpl.class]
- testBServiceImpl: defined in file [E:\IdeaProjects\DemoProjects\springboot-demo\target\classes\com\deepinsea\springbootdemo\service\impl\TestBServiceImpl.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
Process finished with exit code 1
提示说:TestController是单例模式,需要注入单例的Bean,但是找到了2个相同的Bean。
原因
这是因为:默认情况下@Autowired是通过byType的方法注入的。
可是在多个实现类的时候,byType的方式不再是唯一,而需要通过byName的方式来注入,而这个name默认就是根据变量名来的。
解决方案
在Spring的依赖注入的方法有两种,分别是ByType和ByName。
这两者在不同的注解中有不同的优先级顺序,在Spring提供的@Autowired注解中是通过ByType进行注入的,而在JDK提供的@Resource注解中是优先使用ByName,然后ByType进行注入的。
另外,在Spring的依赖声明注解中默认的beanName就是类名首字母小写的名称。
因此,知道了问题的原因,那么相对应的解决方法主要有如下五种:
- 注入service接口具体实现类;
- Autowired+@Qualifier注入;
- @Resource注入;
- 默认的beanName注入;
- ApplicationContext的实现类获取Bean.
上面三种解决方案是最常见的,我们也经常用到,但是有没有考虑直接从spring源码的角度来解决这个问题呢?很明显,最后使用ApplicationContext获取Bean就是从spring源码的角度出发的一种解决方案。
下面我们将会分别对这四种解决方案的原理和实践,分别进行剖析:
前面三种为普遍常用的解决方案,网上一大把实践,因此不作过多分析,重点放在最后一种方式
1. 注入service接口具体实现类
这种方式其实是很好的,既然单例模式下spring容器无法识别同一抽象接口的多个实现类,那么我们直接注入接口的具体实现类即可:
直接将TestController中注入失败的代码进行注释,然后以新的依赖注入方式进行注入即可
package com.deepinsea.springbootdemo.controller;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by deepinsea on 2022/8/22.
* 解决一个Service多个实现类,Bean依赖冲突的问题
*/
@RestController
public class TestController {
// 直接使用@Autowired注入service接口,出现bean冲突的问题
// @Autowired
// private TestService testService;
//
// @GetMapping("")
// public void test(){
// String t1 = testService.test();
// System.out.println(t1);
// }
// 1.注入不同的实现类
@Autowired
private TestAServiceImpl testAService;
@Autowired
private TestBServiceImpl testBService;
@GetMapping("")
public void test(){
String t1 = testAService.test();
System.out.println(t1);
String t2 = testBService.test();
System.out.println(t2);
}
}
使用curl命令进行测试:
C:\Users\deepinsea>curl http://localhost:8080
控制台输出,如下所示:
. ____ _ __ _ _
/\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )___ | '_ | '_| | '_ / _` | \ \ \ \
\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |___, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.6)
2022-08-29 01:11:53.002 INFO 42856 --- [ main] c.d.s.SpringbootDemoApplication : Starting SpringbootDemoApplication using Java 1.8.0_311 on aries with PID 42856 (E:\IdeaProjects\DemoProjects\springboot-demo\target\classes started by deepinsea in E:\IdeaProjects\DemoProjects\springboot-demo)
2022-08-29 01:11:53.005 INFO 42856 --- [ main] c.d.s.SpringbootDemoApplication : No active profile set, falling back to 1 default profile: "default"
2022-08-29 01:11:53.529 INFO 42856 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2022-08-29 01:11:53.535 INFO 42856 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-08-29 01:11:53.535 INFO 42856 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.60]
2022-08-29 01:11:53.636 INFO 42856 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-08-29 01:11:53.636 INFO 42856 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 602 ms
2022-08-29 01:11:53.839 INFO 42856 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2022-08-29 01:11:53.845 INFO 42856 --- [ main] c.d.s.SpringbootDemoApplication : Started SpringbootDemoApplication in 1.095 seconds (JVM running for 2.15)
2022-08-29 01:12:17.839 INFO 42856 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-08-29 01:12:17.839 INFO 42856 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-08-29 01:12:17.839 INFO 42856 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
A
B
通过这种方式,多实现类Bean依赖注入成功!
2. Autowired+@Qualifier注入
如果有多种实现类,那可以通过搭配@Qualifier注解通过ByName的方式进行注入,并且需要Service实现类中也自定义beanName。
我们将beanName自定义为TestAServiceImpl(为了方便记忆,就是实现类的类名)和TestBServiceImpl:
TestAServiceImpl
package com.deepinsea.springbootdemo.service.impl;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.stereotype.Service;
/**
* Created by deepinsea on 2022/8/22.
*/
//@Service
@Service("TestAServiceImpl")
public class TestAServiceImpl implements TestService {
@Override
public String test() {
return "A";
}
}
TestBServiceImpl
package com.deepinsea.springbootdemo.service.impl;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.stereotype.Service;
/**
* Created by deepinsea on 2022/8/22.
*/
//@Service
@Service("TestBServiceImpl")
public class TestBServiceImpl implements TestService {
@Override
public String test() {
return "B";
}
}
然后,在TestController中分别进行注入并调用实现方法:
TestController
package com.deepinsea.springbootdemo.controller;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by deepinsea on 2022/8/22.
* 解决一个Service多个实现类,Bean依赖冲突的问题
*/
@RestController
public class TestController {
// 直接使用@Autowired注入service接口,出现bean冲突的问题
// @Autowired
// private TestService testService;
//
// @GetMapping("")
// public void test(){
// String t1 = testService.test();
// System.out.println(t1);
// }
// 1.注入不同的实现类
// @Autowired
// private TestAServiceImpl testAService;
//
// @Autowired
// private TestBServiceImpl testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 2.使用@Service+@Autowired+@Qualifier声明、注入ByName的Bean,解决Bean依赖冲突
@Autowired
@Qualifier("TestAServiceImpl") //注意:Bean声明方没有设置beanName时,注入名称必须为为class名称
private TestService testAService;
@Autowired
@Qualifier("TestBServiceImpl")
private TestService testBService;
@GetMapping("")
public void test(){
String t1 = testAService.test();
System.out.println(t1);
String t2 = testBService.test();
System.out.println(t2);
}
}
使用curl命令进行测试:
C:\Users\deepinsea>curl http://localhost:8080
控制台输出,如下所示(省略启动日志部分了,上面有展示过):
2022-08-29 01:23:33.442 INFO 15684 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-08-29 01:23:33.442 INFO 15684 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-08-29 01:23:33.443 INFO 15684 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
A
B
这种方式同样注入成功!
3. @Resource注入
@Resource注解由J2EE提供,需要导入包javax.annotation.Resource。
@Resource 可以通过 byName 和 byType的方式注入, 默认先按 byName的方式进行匹配;如果匹配不到,再按 byType的方式进行匹配。
同样的,service实现类中也需要自定义beanName。然后在注入时,声明需要注入的beanName即可:
package com.deepinsea.springbootdemo.controller;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* Created by deepinsea on 2022/8/22.
* 解决一个Service多个实现类,Bean依赖冲突的问题
*/
@RestController
public class TestController {
// 直接使用@Autowired注入service接口,出现bean冲突的问题
// @Autowired
// private TestService testService;
//
// @GetMapping("")
// public void test(){
// String t1 = testService.test();
// System.out.println(t1);
// }
// 1.注入不同的实现类
// @Autowired
// private TestAServiceImpl testAService;
//
// @Autowired
// private TestBServiceImpl testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 2.使用@Service+@Autowired+@Qualifier声明、注入ByName的Bean,解决Bean依赖冲突
// @Autowired
// @Qualifier("TestAServiceImpl")
// private TestService testAService;
//
// @Autowired
// @Qualifier("TestBServiceImpl")
// private TestService testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 3.使用@Service+@Resource声明、注入ByName的Bean,解决Bean依赖冲突
@Resource(name = "TestAServiceImpl")
private TestService testAService;
@Resource(name = "TestBServiceImpl")
private TestService testBService;
@GetMapping("")
public void test(){
String t1 = testAService.test();
System.out.println(t1);
String t2 = testBService.test();
System.out.println(t2);
}
}
使用curl命令进行测试:
C:\Users\deepinsea>curl http://localhost:8080
控制台输出,如下所示:
2022-08-29 01:26:39.557 INFO 40640 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-08-29 01:26:39.557 INFO 40640 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-08-29 01:26:39.558 INFO 40640 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
A
B
测试成功!
4. 默认的beanName注入
像@Service、@Controller、@Configuration和@Component等Spring常用的注解,默认的beanName就是类名首字母小写。
因此,我们可以直接使用默认的beanName进行注入,存储注入Bean的变量名与Spinrg默认设置的beanName相匹配,即可不用自己主动声明注入的beanName。
首先我们可以无需在Service实现类中自定义beanName了,因为我们直接使用Spring默认设置的beanName进行注入:
TestAServiceImpl
package com.deepinsea.springbootdemo.service.impl;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.stereotype.Service;
/**
* Created by deepinsea on 2022/8/22.
*/
@Service
//@Service("TestAServiceImpl")
public class TestAServiceImpl implements TestService {
@Override
public String test() {
return "A";
}
}
TestBServiceImpl
package com.deepinsea.springbootdemo.service.impl;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.stereotype.Service;
/**
* Created by deepinsea on 2022/8/22.
*/
@Service
//@Service("TestBServiceImpl")
public class TestBServiceImpl implements TestService {
@Override
public String test() {
return "B";
}
}
然后,我们直接使用默认的beanName以多种方式分别进行注入:
① @Qualifier或@Resource注入默认名称的Bean
第一种方式:@Qualifier或@Resource注入默认名称的Bean
package com.deepinsea.springbootdemo.controller;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* Created by deepinsea on 2022/8/22.
* 解决一个Service多个实现类,Bean依赖冲突的问题
*/
@RestController
public class TestController {
// 直接使用@Autowired注入service接口,出现bean冲突的问题
// @Autowired
// private TestService testService;
//
// @GetMapping("")
// public void test(){
// String t1 = testService.test();
// System.out.println(t1);
// }
// 1.注入不同的实现类
// @Autowired
// private TestAServiceImpl testAService;
//
// @Autowired
// private TestBServiceImpl testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 2.使用@Service+@Autowired+@Qualifier声明、注入ByName的Bean,解决Bean依赖冲突
// @Autowired
// @Qualifier("TestAServiceImpl")
// private TestService testAService;
//
// @Autowired
// @Qualifier("TestBServiceImpl")
// private TestService testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 3.使用@Service+@Resource声明、注入ByName的Bean,解决Bean依赖冲突
// @Resource(name = "TestAServiceImpl")
// private TestService testAService;
//
// @Resource(name = "TestBServiceImpl")
// private TestService testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 4.使用默认的beanName注入
// (1).直接通过@Qualifier或@Resource注入默认名称的Bean,解决Bean依赖冲突(推荐,这种方式不用更改原类,只需注入端配合即可)
// Spring中用@Component、@Repository、@Service和 @Controller等标注的默认Bean名称是小写开头的类名
// 注意:无论哪一种方式,都需要对所有冲突的Bean声明Bean的名称
@Resource(name = "testAServiceImpl")
private TestService testAService;
@Resource(name = "testBServiceImpl")
private TestService testBService;
@GetMapping("")
public void test(){
String t1 = testAService.test();
System.out.println(t1);
String t2 = testBService.test();
System.out.println(t2);
}
}
使用curl命令进行测试:
C:\Users\deepinsea>curl http://localhost:8080
控制台输出,如下所示:
A
B
测试成功!
② 默认的beanName作为Service对象存储的变量名称注入
第二种方式:直接使用默认的beanName作为Service对象存储的变量名称注入
package com.deepinsea.springbootdemo.controller;
import com.deepinsea.springbootdemo.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by deepinsea on 2022/8/22.
* 解决一个Service多个实现类,Bean依赖冲突的问题
*/
@RestController
public class TestController {
// 直接使用@Autowired注入service接口,出现bean冲突的问题
// @Autowired
// private TestService testService;
//
// @GetMapping("")
// public void test(){
// String t1 = testService.test();
// System.out.println(t1);
// }
// 1.注入不同的实现类
// @Autowired
// private TestAServiceImpl testAService;
//
// @Autowired
// private TestBServiceImpl testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 2.使用@Service+@Autowired+@Qualifier声明、注入ByName的Bean,解决Bean依赖冲突
// @Autowired
// @Qualifier("TestAServiceImpl")
// private TestService testAService;
//
// @Autowired
// @Qualifier("TestBServiceImpl")
// private TestService testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 3.使用@Service+@Resource声明、注入ByName的Bean,解决Bean依赖冲突
// @Resource(name = "TestAServiceImpl")
// private TestService testAService;
//
// @Resource(name = "TestBServiceImpl")
// private TestService testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 4.使用默认的beanName注入
// (1).直接通过@Qualifier或@Resource注入默认名称的Bean,解决Bean依赖冲突(推荐,这种方式不用更改原类,只需注入端配合即可)
// Spring中用@Component、@Repository、@Service和 @Controller等标注的默认Bean名称是小写开头的类名
// 注意:无论哪一种方式,都需要对所有冲突的Bean声明Bean的名称
// @Resource(name = "testAServiceImpl")
// private TestService testAService;
//
// @Resource(name = "testBServiceImpl")
// private TestService testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// (2).直接使用默认的beanName作为Service对象存储的变量名称注入(推荐)
@Autowired
//@Resource
private TestService testAServiceImpl;
@Autowired
//@Resource
private TestService testBServiceImpl;
@GetMapping("")
public void test(){
String t1 = testAServiceImpl.test();
System.out.println(t1);
String t2 = testBServiceImpl.test();
System.out.println(t2);
}
}
使用curl命令进行测试:
C:\Users\deepinsea>curl http://localhost:8080
控制台输出,如下所示:
2022-08-29 01:26:39.557 INFO 40640 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-08-29 01:26:39.557 INFO 40640 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-08-29 01:26:39.558 INFO 40640 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
A
B
测试成功!
5. ApplicationContext的实现类获取Bean
最后一种方式,是通过获取Spring上下文(ApplicationContext)的方式来实现的。
其实这种方式,才是最符合spring源码运行规律的最佳方式,上面的方案只能说是一种类似"补丁"的解决方案。
下面我们从源码出发,深入剖析下以下三个问题:
- ApplicationContext是什么?
- ApplicationContext与BeanFactory的关系?
- 为什么ApplicationContext能获取到容器的Bean?
① ApplicationContext是什么
ApplicationContext是比BeanFactory更加强大的Spring容器,它既可以创建bean、获取bean、还支持国际化、事件广播、获取资源等BeanFactory不具备的功能。
Spring容器的理解
Spring容器可以理解为生产对象(Object)的地方,在这里容器不只是帮我们创建了对象那么简单,它负责了对象的整个生命周期--创建、装配、销毁。而这里对象的创建管理的控制权都交给了Spring容器,所以这是一种控制权的反转,称为IOC容器,而这里IOC容器不只是Spring才有,很多框架也都有该技术。
② ApplicationContext与BeanFactory的关系
Spring 作为bean容器的印象似乎已经深入人心,但Spring 首先是一个应用框架,然后才会是一个组件容器。 Spring 的核心接口ApplicationContext
作为应用的一方面从接口定义来看应该是明显的。
从ApplicationContext接口本身来看:
public interface ApplicationContext extends
EnvironmentCapable, // 继承环境对象容器接口
ListableBeanFactory,
HierarchicalBeanFactory, // 继承beanFactory
MessageSource, // 集成消息解析器
ApplicationEventPublisher, // 继承应用事件发布器
ResourcePatternResolver // 继承模式资源解析器
{
@Nullable
String getId();
String getApplicationName();
String getDisplayName();
long getStartupDate();
@Nullable
ApplicationContext getParent();
AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}
ApplicationContext的继承关系类图
从上图中可以发现,ApplicationContext接口继承了很多接口,这些接口我们可以将其分为5类:
- MessageSource,主要用于国际化
- ApplicationEventPublisher,提供了事件发布功能
- EnvironmentCapable,可以获取容器当前运行的环境
- ResourceLoader,主要用于加载资源文件
- BeanFactory,负责配置,创建,管理bean,IOC功能的实现主要就依赖于该接口子类实现
关于这些接口的具体功能的介绍在后文会介绍,当前我们需要知道最重要的一点就是ApplicationContext继承了BeanFactory接口,也就是或它具有BeanFactory所有的功能。
ApplicationContext常用实现类及其作用
ApplicationContext常用实现类 | 作用 |
---|---|
AnnotationConfigApplicationContext | 从一个或多个基于java的配置类中加载上下文定义,适用于java注解的方式。 |
ClassPathXmlApplicationContext | 从类路径下的一个或多个xml配置文件中加载上下文定义,适用于xml配置的方式。 |
FileSystemXmlApplicationContext | 从文件系统下的一个或多个xml配置文件中加载上下文定义,也就是说系统盘符中加载xml配置文件。 |
AnnotationConfigWebApplicationContext | 专门为web应用准备的,适用于注解方式。 |
XmlWebApplicationContext | 从web应用下的一个或多个xml配置文件加载上下文定义,适用于xml配置方式。 |
Spring具有非常大的灵活性,它提供了三种主要的装配机制:
-
在XMl中进行显示配置
-
在Java中进行显示配置
-
隐式的bean发现机制和自动装配
- 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。
- 自动装配(autowiring):Spring自动满足bean之间的依赖。
使用的优先性: 3>2>1,尽可能地使用自动配置的机制,显示配置越少越好。当必须使用显示配置bean的时候(如:有些源码不是由你来维护的,而当你需要为这些代码配置bean的时候),推荐使用类型安全比XML更加强大的JavaConfig。最后只有当你想要使用便利的XML命名空间,并且在JavaConfig中没有同样的实现时,才使用XML。
BeanFactory和ApplicationContext的区别
1、国际化 BeanFactory是不支持国际化功能的,因为BeanFactory没有扩展Spring中MessageResource接口。相反,由于ApplicationContext扩展了MessageResource接口,因而具有消息处理的能力(i18N)
2、强大的事件机制(Event) 基本上牵涉到事件(Event)方面的设计,就离不开观察者模式, ApplicationContext的事件机制主要通过ApplicationEvent和ApplicationListener这两个接口来提供的,和java swing中的事件机制一样。即当ApplicationContext中发布一个事件的时,所有扩展了ApplicationListener的Bean都将会接受到这个事件,并进行相应的处理。
3、底层资源的访问 ApplicationContext扩展了ResourceLoader(资源加载器)接口,从而可以用来加载多个Resource,而BeanFactory是没有扩展ResourceLoader
4、对Web应用的支持 与BeanFactory通常以编程的方式被创建不同的是,ApplicationContext能以声明的方式创建,如使用ContextLoader。当然你也可以使用ApplicationContext的实现之一来以编程的方式创建ApplicationContext实例 。
5、延迟加载 1).BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化,这样,我们就不能发现一些存在的spring的配置问题。而ApplicationContext则相反,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误。
2).BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册
可以看到,ApplicationContext继承了BeanFactory,BeanFactory是Spring中比较原始的Factory,它不支持AOP、Web等Spring插件,而ApplicationContext不仅包含了BeanFactory的所有功能,还支持Spring的各种插件,还以一种面向框架的方式工作以及对上下文进行分层和实现继承。 BeanFactory是Spring框架的基础设施,面向Spring本身;而ApplicationContext面向使用Spring的开发者。
相比于BeanFactory,ApplicationContext提供了更多面向实际应用的功能,几乎所有场合都可以直接使用ApplicationContext而不是底层的BeanFactory。
③ 为什么ApplicationContext能获取到容器的Bean
因为ApplicationContext继承了BeanFactory接口,也就是说ApplicationContext拥有BeanFactory的所有方法。
因此,可以直接使用Application实现类中getBean方法的来获取Bean容器中的Bean对象。
参考
- Spring源码分析(十一)ApplicationContext详细介绍(上)
- Spring 中的 ApplicationContext
- Spring中 BeanFactory和ApplicationContext的区别
- 理解Spring容器、BeanFactory和ApplicationContext
代码实践
从上面对Spring源码中的ApplicationContext上下文(容器)的理解,可知:
- ApplicationContext继承了BeanFactory接口;
- ApplicationContext的AnnotationConfigApplicationContext实现类可以针对注解方式加载Bean上下文定义;
- AnnotationConfigApplicationContext的getBean方法可以获取Bean详情.
因此,我们来通过ApplicationContext接口来实现同一接口多实现类依赖注入:
同样的,实现类也不需要主动自定义声明beanName。
TestController
package com.deepinsea.springbootdemo.controller;
import com.deepinsea.springbootdemo.service.impl.TestAServiceImpl;
import com.deepinsea.springbootdemo.service.impl.TestBServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by deepinsea on 2022/8/22.
* 解决一个Service多个实现类,Bean依赖冲突的问题
*/
@RestController
public class TestController {
// 直接使用@Autowired注入service接口,出现bean冲突的问题
// @Autowired
// private TestService testService;
//
// @GetMapping("")
// public void test(){
// String t1 = testService.test();
// System.out.println(t1);
// }
// 1.注入不同的实现类
// @Autowired
// private TestAServiceImpl testAService;
//
// @Autowired
// private TestBServiceImpl testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 2.使用@Service+@Autowired+@Qualifier声明、注入ByName的Bean,解决Bean依赖冲突
// @Autowired
// @Qualifier("TestAServiceImpl")
// private TestService testAService;
//
// @Autowired
// @Qualifier("TestBServiceImpl")
// private TestService testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 3.使用@Service+@Resource声明、注入ByName的Bean,解决Bean依赖冲突
// @Resource(name = "TestAServiceImpl")
// private TestService testAService;
//
// @Resource(name = "TestBServiceImpl")
// private TestService testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// 4.使用默认的beanName注入
// (1).直接通过@Qualifier或@Resource注入默认名称的Bean,解决Bean依赖冲突(推荐,这种方式不用更改原类,只需注入端配合即可)
// Spring中用@Component、@Repository、@Service和 @Controller等标注的默认Bean名称是小写开头的类名
// 注意:无论哪一种方式,都需要对所有冲突的Bean声明Bean的名称
// @Resource(name = "testAServiceImpl")
// private TestService testAService;
//
// @Resource(name = "testBServiceImpl")
// private TestService testBService;
//
// @GetMapping("")
// public void test(){
// String t1 = testAService.test();
// System.out.println(t1);
// String t2 = testBService.test();
// System.out.println(t2);
// }
// (2).直接使用默认的beanName作为Service对象存储的变量名称注入(推荐)
// @Autowired
// //@Resource
// private TestService testAServiceImpl;
//
// @Autowired
// //@Resource
// private TestService testBServiceImpl;
//
// @GetMapping("")
// public void test(){
// String t1 = testAServiceImpl.test();
// System.out.println(t1);
// String t2 = testBServiceImpl.test();
// System.out.println(t2);
// }
// 5.使用ApplicationContext上下文类获取Bean(因为ApplicationContext接口继承了BeanFactory,因此可以使用其所有方法)
@GetMapping("")
public void test(){
//获取实现类A的Bean,并调用接口方法
ApplicationContext contextA = new AnnotationConfigApplicationContext(TestAServiceImpl.class);
TestAServiceImpl testAService = contextA.getBean(TestAServiceImpl.class);
System.out.println(testAService.test());
//获取实现类B的Bean,并调用接口方法
ApplicationContext contextB = new AnnotationConfigApplicationContext(TestBServiceImpl.class);
TestBServiceImpl testBService = contextB.getBean(TestBServiceImpl.class);
System.out.println(testBService.test());
}
}
使用curl命令进行测试:
C:\Users\deepinsea>curl http://localhost:8080
控制台输出,如下所示:
A
B
测试成功!
总结
1、@Autowired 是通过 byType 的方式去注入的, 使用该注解,要求接口只能有一个实现类;
2、@Resource 可以通过 byName 和 byType的方式注入, 默认先按 byName的方式进行匹配,如果匹配不到,再按 byType的方式进行匹配;
3、@Qualifier 注解配合@Autowired 一起使用;
4、通过name匹配时不要忘记给实现类加名字。
另外,采用ApplicationContext的方式虽然比较高级,但是不一定保证项目组中其他人也会使用这种方式。在项目组成员技术水平不一致的情况下,如果不愿意牺牲代码同步规范的话,还是推荐和项目组其他成员使用相同的方式即可。
欢迎关注白羊,感谢观看ヾ(◍°∇°◍)ノ゙
转载自:https://juejin.cn/post/7136999582965170213