【Spring 核心: IoC&DI】从原理到注解使用、注入方式全攻略

一、定义IoC&DI

1.1 IocSpring 是包含了众多工具方法的IoC容器。IoC 是Spring的核心思想,例如前面章节所示:在类上面添加@RestController 和 @Controller 注解,,就是把这个对象交给Spring管理,Spring框架启动时就会加载该类。把对象交给Spring管理,就是IoC思想。

IoC :Inversion of Control (控制反转),也就是说Spring是一个"控制反转"的容器。控制反转也就是 控制权 反转。

获得依赖对象的过程被反转了,也就是说,当需要某个对象时,传统开发模式中需要自己通过new创建对象,现在不需要再进行创建,把创建对象的任务交给容器,程序中只需要依赖注入 (DependencyInjection,DI)就可以了。

这个容器称为:IoC容器。Spring是⼀个IoC容器,所以有时Spring也称为Spring容器。

下面进行 IoC 案例说明

1.1.1 传统程序开发实现下面的需求:从右向左依次依赖。

在这里插入图片描述按照每一个模块设计一个类,代码如下:

代码语言:javascript复制public class Car {

private Framework framework;

public Car(){

framework = new Framework();

System.out.println("car init...");

}

public void run() {

System.out.println("car run...");

}

}

public class Framework {

private Bottom bottom;

public Framework(){

bottom = new Bottom();

System.out.println("Framework init...");

}

}

public class Bottom {

private Tire tire;

public Bottom(){

tire = new Tire();

System.out.println("Bottom init...");

}

}

public class Tire {

private int size;

public Tire(){

this.size = 17;

System.out.println("size : " + size);

}

}这样设计的可维护性太低,耦合度太高,当需求变更时,对上述代码修改起来会非常麻烦,当修改尺寸时,修改如下:

在这里插入图片描述从上述得出,当底层代码改动后,整个链上的所有代码都需要进行修改。

解决方案

可以尝试不在每个类中自己创建下级类,如果自己创建下级类就会出现当下级类发生改变操作,

自己也要跟着修改。此时,我们只需要将原来由自己创建的下级类,改为传递的方式(也就是注入的方式),因为我们不需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修改任何代码,这样就完成了程序的解耦。1.1.2 IoC 程序开发基于上述方案,将程序进行改造,将创建子类的方式改为注入传递的方式,代码如下:

代码语言:javascript复制public class Main {

public static void main(String[] args) {

Tire tire = new Tire(17);

Bottom bottom = new Bottom(tire);

Framework framework = new Framework(bottom);

Car car = new Car(framework);

car.run();

}

}

public class Tire {

private int size;

public Tire(int size){

this.size = size;

System.out.println("size : " + size);

}

}

public class Bottom {

private Tire tire;

public Bottom(Tire tire){

this.tire = tire;

System.out.println("Bottom init...");

}

}

public class Framework {

private Bottom bottom;

public Framework(Bottom bottom){

this.bottom = bottom;

System.out.println("Framework init...");

}

}

public class Car {

private Framework framework;

public Car(Framework framework){

this.framework = framework;

System.out.println("car init...");

}

public void run() {

System.out.println("car run...");

}

}经过以上调整,无论底层类中如何变化,整个调用链不用做任何变化,就完成了代码之间的解耦合,使得更加灵活。这就是IoC思想。

1.1.3 IoC 优势通过上述示例发现,通用程序的实现代码,类的创建顺序是反的,传统代码是Car控制并创建了 Framework,Framework创建并创建了Bottom,依次往下,而改进之后的控制权发生的反转,不再是使用方对象创建并控制依赖对象了,而是把依赖对象注入将当前对象中,依赖对象的控制权不再由当前类控制了。这样的话,即使依赖类发生任何改变,当前类都是不受影响的,这就是典型的控制反转,也就是IoC的实现思想。IoC容器也就是控制反转器。IoC 容器具有以下优点:

资源集中管理:IoC容器会帮我们管理⼀些资源(对象等),我们需要使用时,只需要从IoC容器中去取就可以了。在创建实例的时候不需要了解其中的细节,降低了使用资源双方的依赖程度,也就是耦合度。1.2 DIDI: DependencyInjection(依赖注入)

容器在运行期间,动态的为应用程序提供运行时所依赖的资源,称之为依赖注入。

上述示例代码就是通过构造函数的方式,将依赖对象注入到需要使用的对象中的。

IoC 是一种思想,也是"目标",而思想只是一种指导原则,最终还是要有可行的落地方案,而DI就属于

具体的实现。所以也可以说,DI是IoC的一种实现。

二、IoC 详解2.1 Bean 的存储Spring 框架为了更好的服务Web应用程序,提供了更丰富的注解。

共有两类注解类型:

类注解:@Controller、@Service、@Repository、@Component、@Configuration方法注解:@Bean2.2 @Controller (控制器存储)使用@Controller 注解存储Bean ,代码如下:

代码语言:javascript复制@Controller

public class UserController {

public void sayHello(){

System.out.println("do controller..");

}

}加上注解后,此时这个对象就已经存储在Spring容器中了。

通过获取Spring上下文对象:发现成功获取到Controller对象

代码语言:javascript复制public class SpringIocDemoApplication {

public static void main(String[] args) {

ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);

UserController bean = context.getBean(UserController.class);

System.out.println(bean);

}

}在这里插入图片描述获取Bean 对象的其他方式,ApplicationContext 也提供了其他方式进行获取:在这里插入图片描述 可以根据名称来获取bean,那么bean 命名的规则是?

五大类注解让Spring管理Bean对象的默认取名方式如下:

bean名称以小写字母开头,小驼峰命名。例:UserController, Bean的名称为:userController当有多个字符并且第一个和第二个字符都是大写时,Bean对象名就是类名。例:UController, Bean的名称为:UController2.3 @Service(服务存储)使用@Service 注解存储bean代码如下,获取bean对象方式不变:

代码语言:javascript复制@Service

public class UserService {

public void doService(){

System.out.println("do service...");

}

}2.4 @Repository (仓库存储)使用@Repository 注解存储bean代码如下,获取bean对象方式不变:

代码语言:javascript复制@Repository

public class UserRepository {

public String doRepository(){

return "do repository...";

}

}2.5 @Component(组件存储)使用@Component 注解存储bean代码如下,获取bean对象方式不变:

代码语言:javascript复制@Component

public class UserComponent {

public String doComponent(){

return "do component...";

}

}2.6 @Configuration(配置存储)使用@Configuration 注解存储bean代码如下,获取bean对象方式不变:

代码语言:javascript复制@Configuration

public class UserConfig {

public String doConfig(){

return "do config...";

}

}2.7 五大类注解区别 为什么要用这么多类注解呢,既然都是将对象交给Spring容器,使用哪个注解功能不是都一样吗,这样使用不同的注解是为了什么?

使用这么多类注解是为了应用的分层,当看到这些不同的注解,就能直接了解当前类的用途:

@Controller:控制层,接收请求,对请求进行处理,并进行响应.@Service:业务逻辑层,处理具体的业务逻辑.@Repository:数据持久层,也称为持久层.负责数据访问操作@Configuration:配置层.处理项目中的一些配置信息.@Controller,@Service,@Repository,@Configuration这四个注解都是@Component注解的衍生注解,可以理解为是父子关系。

2.8 方法注解 @Bean类注解是添加到某个类上,但是类注解存在问题:

使用外部包中类,没办法添加类注解一个类,需要多个对象,无法使用类注解此时需要使用方法注解 @Bean

使用示例:

代码语言:javascript复制@Configuration

public class BeanConfig {

@Bean

public UserInfo user1(){

return new UserInfo("zhangsan",17);

}

}方法注解 @Bean 需要配合类注解才能将对象正常的存储到Spring容器中。

通过获取Spring上下文对象可以得到:代码语言:javascript复制@SpringBootApplication

public class SpringIocDemoApplication {

public static void main(String[] args) {

ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);

UserInfo bean1 = context.getBean("user1",UserInfo.class);

System.out.println(bean1);

}

}在这里插入图片描述2.8.1 重命名 Bean可以通过设置name属性给Bean对象进行重命名操作,如下代码示例:

代码语言:javascript复制@Configuration

public class BeanConfig {

@Bean(name = {"u1","userInfo"})

public UserInfo user1(){

return new UserInfo("zhangsan",17);

}

}此时可以使用 u1 获取UserInfo 对象:

代码语言:javascript复制@SpringBootApplication

public class SpringIocDemoApplication {

public static void main(String[] args) {

ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);

UserInfo bean = (UserInfo) context.getBean("u1");

System.out.println(bean);

}

}在这里插入图片描述 重命名代码也可以进行省略:@Bean({"u1","userInfo"}),@Bean("u1")

2.9 扫描路径使用五大注解声明的Bean,生效必须配置扫描路径,让Spring扫描到这些注解,也就是通过 @ComponentScan 来配置这些路径。

但是默认扫描范围是SpringBoot 启动类所在的包及其子包,因此将启动类 SpringIocDemoApplication 放在希望扫描的包的路径下,那么定义的Bean 就都可以被扫描到。

三、DI 详解依赖注入是一个过程,是指IoC容器在创建Bean时,去提供运行时所依赖的资源,而资源指的就是对象。简单来说,就是把对象取出来放到某个类的属性中。

关于依赖注入,Spring 提供了三种方式:

属性注入构造方法注入Setter 注入3.1 属性注入属性注入使用 @Autowired 实现,示例:

代码语言:javascript复制@Service

public class UserService {

@Autowired

private UserInfo userInfo;

public void doService(){

System.out.println(userInfo);

System.out.println("do service...");

}

}代码语言:javascript复制@SpringBootApplication

public class SpringIocDemoApplication {

public static void main(String[] args) {

ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);

UserService bean = context.getBean(UserService.class);

bean.doService();

}

}在这里插入图片描述3.2 构造方法注入构造方法注入是在类的构造方法中实现注入,示例:

代码语言:javascript复制@Controller

public class UserController {

//构造方法注入

private UserService userService;

public UserController(){

}

@Autowired

public UserController(UserService userService){

this.userService = userService;

}

public void sayHello(){

userService.doService();

System.out.println("do controller..");

}

}当类中只有一个构造方法时,那么 @Autowired 注解可以省略;当类中有多个构造方法时,那么需要加上 @Autowired 明确指定使用哪个构造方法。只能在一个构造方法上加上 @Autowired 注解。3.3 Setter 注入Setter 注入和属性的 Setter 方法实现类似,只是在set方法时加上 @Autowired 注解,示例:

代码语言:javascript复制@Controller

public class UserController {

//Setter 注入

private UserService userService;

@Autowired

public void setUserService(UserService userService) {

this.userService = userService;

}

public void sayHello(){

userService.doService();

System.out.println("do controller..");

}

}3.4 三种注入方式优缺点属性注入:

优点:简洁,使用方便

缺点:是能用于IoC 容器,并且只有在使用时才会出现异常;不能注入一个Final 修饰的属性。构造函数注入

优点:可以注入Final 修饰的属性;注入的对象不会被修改;依赖对象在使用前一定会被完全初始化;通用性好,适用于所有框架。

缺点:注入多个对象时,代码比较繁琐,需要多个对象参数。Setter 注入

优点:方便在类示例之后重新对该对象进行配置或注入。

缺点:不能注入一个 Final 修饰的属性;注入对象可能会被改变,因为Setter 方法可能会被多次调用。3.5 @Autowired 注解问题及解决当同一类型存在多个bean时,使用@Autowired 会存在问题:

代码语言:javascript复制@Configuration

public class BeanConfig {

@Bean({"u1"})

public UserInfo user1(){

return new UserInfo("zhangsan",17);

}

@Bean({"u2"})

public UserInfo user2(){

return new UserInfo("lisi",20);

}

}代码语言:javascript复制@Service

public class UserService {

@Autowired

private UserInfo userInfo;

public void doService(){

System.out.println(userInfo);

System.out.println("do service...");

}

}代码语言:javascript复制@SpringBootApplication

public class SpringIocDemoApplication {

public static void main(String[] args) {

ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);

UserService bean = context.getBean(UserService.class);

bean.doService();

}

}此时,当有两个userInfo 对象时,获取上下文发现启动失败,信息如下:

在这里插入图片描述 报错原因是需要一个单一对象,但是找到了两个,因此Spring 提供了以下几种解决方案。

3.5.1 @Primary 解决使用 @Primary 注解:当存在多个相同类型的Bean 注入时,加上 @Primary 注解,来确定默认Bean的实现。示例:

代码语言:javascript复制@Configuration

public class BeanConfig {

@Bean({"u1"})

public UserInfo user1(){

return new UserInfo("zhangsan",17);

}

@Bean({"u2"})

@Primary

public UserInfo user2(){

return new UserInfo("lisi",20);

}

}此时默认获取到的对象就是 user2 。

3.5.2 @Qualifier 解决使用@Qualifier 注解:指定当前要注入的Bean对象。在其 value 属性中,指定注入bean对象的名称。

@Qualifier 注解不能单独使用,必须配合@Autowired 注解使用。代码语言:javascript复制@Service

public class UserService {

@Autowired

@Qualifier("u1")

private UserInfo userInfo;

public void doService(){

System.out.println(userInfo);

System.out.println("do service...");

}

}此时获取到对象为 user1。

3.5.3 @Resource 解决使用@Resource 注解:是按照Bean 的名称进行注入。通过name属性指定要注入的bean 的名称。示例:

代码语言:javascript复制@Service

public class UserService {

@Resource(name = "u2")

private UserInfo userInfo;

public void doService(){

System.out.println(userInfo);

System.out.println("do service...");

}

}此时获取到的对象为 user2。

3.5.4 @Autowired 与 @Resource 区别@Autowired 是Spring框架提供的注解,而 @Resource 是JDK 提供的注解。@Autowired 默认是按照类型注入,而@Resource 是按照名称注入。相比于@Autowired 来说,@Resource 支持更多的参数设置,例如name设置,根据名称获取Bean。