likes
comments
collection
share

【源码专题】Spring源码深入剖析之ApplicationContext解决多实现类依赖注入问题

作者站长头像
站长
· 阅读数 15

本文将从解决多实现类依赖注入问题,深入剖析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的依赖注入的方法有两种,分别是ByTypeByName

这两者在不同的注解中有不同的优先级顺序,在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的继承关系类图

【源码专题】Spring源码深入剖析之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上下文(容器)的理解,可知:

  • 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的方式虽然比较高级,但是不一定保证项目组中其他人也会使用这种方式。在项目组成员技术水平不一致的情况下,如果不愿意牺牲代码同步规范的话,还是推荐和项目组其他成员使用相同的方式即可。

欢迎关注白羊,感谢观看ヾ(◍°∇°◍)ノ゙