【JavaEE】Spring(3):IoC和DI


一、什么是IoC

IoC:控制反转,在传统的开发模式中,我们需要自己通过new创建和管理对象,而在IoC的开发模式中,我们将创建和管理对象的任务交给容器,Spring就是一个IoC容器

IoC是一种思想,接下来以传统的Java开发实现IoC的角度来讲解

1.1 传统Java开发

我们以制造汽车为例:

我们制造一个汽车,需要先制造它的车身,要制造车身,需要先制造底盘,要制造底盘,需要先制造轮胎

java 复制代码
public class NewCarExample {
    public static void main(String[] args) {
        Car car = new Car(21);
        car.run();
    }
    /**
     * 汽⻋对象
     */
    static class Car {
        private Framework framework;
        public Car(int size) {
            framework = new Framework(size);
            System.out.println("Car init....");
        }
        public void run(){
            System.out.println("Car run...");
        }
    }
    /**
     * ⻋⾝类
     */
    static class Framework {
        private Bottom bottom;
        public Framework(int size) {
            bottom = new Bottom(size);
            System.out.println("Framework init...");
        }
    }
    /**
     * 底盘类
     */
    static class Bottom {
        private Tire tire;
        public Bottom(int size) {
            this.tire = new Tire(size);
            System.out.println("Bottom init...");
        }
    }
    /**
     * 轮胎类
     */
    static class Tire {
        // 尺⼨
        private int size;
        public Tire(int size){
            this.size = size;
            System.out.println("轮胎尺⼨:" + size);
        }
    }
}

上述代码的耦合是很高的,比如又有轮胎颜色的需求,那就要修改整个代码

解决方案:上述代码中,对象的控制权在其使用者:谁用谁去创建相应的对象。我们可以将对象的创建放在一起,然后让使用者创建对象的方式改为传递对象的方式

java 复制代码
public class IocCarExample {
    public static void main(String[] args) {
        Tire tire = new Tire(20);
        Bottom bottom = new Bottom(tire);
        Framework framework = new Framework(bottom);
        Car car = new Car(framework);
        car.run();
    }
    static class Car {
        private Framework framework;
        public Car(Framework framework) {
            this.framework = framework; //在IoC容器中已经创建好了对象只需要从容器中获取
            System.out.println("Car init....");
        }
        public void run() {
            System.out.println("Car run...");
        }
    }
    static class Framework {
        private Bottom bottom;
        public Framework(Bottom bottom) {
            this.bottom = bottom;
            System.out.println("Framework init...");
        }
    }
    static class Bottom {
        private Tire tire;
        public Bottom(Tire tire) {
            this.tire = tire;
            System.out.println("Bottom init...");
        }
    }
    static class Tire {
        private int size;
        public Tire(int size) {
            this.size = size;
            System.out.println("轮胎尺⼨:" + size);
        }
    }
}

将对象的创建放在一起,并以传递对象的方式代替每个类自己创建对象的方式

上图就是IoC容器所做的工作,Spring就是一个IoC容器

1.2 IoC容器的优势

  1. 资源集中管理:IoC容器帮我们管理资源(对象),我们需要时只需要从中获取即可

  2. 降低了使用资源双方的耦合度

1.3 总结

IoC是一种思想,传统模式下,对象的创建和它们之间的依赖关系由程序自己控制,在IoC模式下,控制权交给了外部容器

二、什么是DI

DI:依赖注入,是实现IoC思想的一种具体方式。它侧重于如何将依赖对象提供给需要使用它们的对象,主要关注的是依赖对象进入目标对象的方式,比如通过构造方法、setter 方法或者直接注入到字段等途径,上述代码中是通过构造方法的方式把依赖对象注入到需要使用的对象中

三、Spring IoC

前面我们学过,IoC就是将对象的创建和管理交给IoC容器,而Spring就是一个IoC容器,将对象的创建和管理交给IoC容器,接下来先学习如何将对象存储到Spring,也就是Bean的存储

Bean:Spring管理的对象

3.1 Bean的存储

为了让某个对象交给IoC容器管理,Spring提供了丰富的注解:

  1. 类注解:@Controller、@Service、@Repository、@Component、@Configuration
  2. 方法注解:@Bean

3.1.1 @Controller

通过类将该对象存储到Spring容器

java 复制代码
@RestController
public class UserController {

    @RequestMapping("/hello")
    public void sayHi() {
        System.out.println("hello world");
    }
}

由于 @RestController 包含 @Controller,所以这里不用特意写@Controller,接下来观察该对象是否已经存储到Spring

在该项目的启动类中:

java 复制代码
@SpringBootApplication
public class SpringBootDemo1Application {

	public static void main(String[] args) {
		//1. 获取Spring上下文对象
		ApplicationContext context = SpringApplication.run(SpringBootDemo1Application.class, args);
		//2. 从Spring上下文中获取想要的对象
		UserController userController = context.getBean(UserController.class);
		//3. 使用对象
		userController.sayHi();
	}
}

获取bean的其他方式

上述代码是通过类型来查找对象的,ApplicationContext 也提供了其他获取bean的方式,注意ApplicationContext 获取bean的功能是 BeanFactory提供的,前者是后者的子接口

其他获取bean的方式:

java 复制代码
public interface BeanFactory {
    //...

    // 1. 根据类型获取bean
    <T> T getBean(Class<T> var1) throws BeansException;

    // 2. 根据bean名称获取bean
    Object getBean(String var1) throws BeansException;

    // 3. 根据bean名称和类型获取bean
    <T> T getBean(String var1, Class<T> var2) throws BeansException;


    //...
}

开始使用的第一种方法,第二种方法是根据bean的名称获取

bean 的起名规则

  • 一般情况下,bean名称以小写字母开头,然后使用驼峰式大小写,比如类名:UserController,baen名:userController
  • 也有特殊情况,当类名第一个和第二个字符都是大写,则bean名称和类名一样,比如类名:UController,bean名:UController
java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

	public static void main(String[] args) {
		//1. 获取Spring上下文对象
		ApplicationContext context = SpringApplication.run(SpringBootDemo1Application.class, args);
		//2. 从Spring上下文中获取想要的对象
		UserController userController = context.getBean(UserController.class);
		UserController userController1 = (UserController) context.getBean("userController");
		UserController userController2 = (UserController) context.getBean("userController", UserController.class);

		System.out.println(userController);
		System.out.println(userController1);
		System.out.println(userController2);
}

可以看到从Spring容器中取出的都是相同的对象,所以Spring容器中创建对象采用的是单例模式

3.1.2 @Service

使用@Service存储bean:

java 复制代码
@Service
public class UserService {

    public void sayHi(String name) {
        System.out.println("Hi," + name);
    }
}

读取bean:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //1. 获取Spring上下⽂对象
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        //2. 从上下文中获取UserService对象
        UserService userService = context.getBean(UserService.class);
        //3. 使⽤对象
        userService.sayHi();
    }
}

3.1.3 @Repository

使用@Repository存储bean:

java 复制代码
@Repository
public class UserRepository {

    public void sayHi() {
        System.out.println("Hi, UserRepository~");
    }

}

读取bean:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //1. 获取Spring上下⽂对象
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        //2. 从上下文中获取UserService对象
        UserRepository userRepository= context.getBean(UserRepository.class);
        //3. 使⽤对象
        UserRepository.sayHi();
    }
}

后面的两个类注解都是一样的使用方式,这里不再多赘述

为什么要有这么多类注解

这里就要讲解以下应用分层,在现如今的项目,业务逐渐变得复杂,大量的代码混在一起,会出现逻辑不清晰,代码扩展性差等一系列问题,为了更好的避免这些问题,就需要应用分层

在之前讲的Spring MVC就是将整体系统分为Model (模型)、View (视图)、Controller (控制器) 三层,将用户视图业务处理 分开,并通过控制器连接起来,现在更主流的开发方式是前后端分离 ,因此作为后端开发,又有了一套新的分层体系→三层架构

  • Controller (控制层):接收前端发送的请求,对请求进行处理并返回响应
  • Service (业务逻辑层):处理具体的业务逻辑
  • Dao (数据访问层/持久层):负责数据的访问,包括数据的增、删、改、查等

对项目进行应用分层可以更高效的进行项目开发,回到最开始的问题,为什么要有这么多类注解:其最直接的作用就是让程序员看到类注解后就能直接了解到当前类的用途(其所在的层次)

  • @Controller:控制层,接收请求,对请求进行处理,并进行响应
  • @Servie:业务逻辑层,处理具体的业务逻辑
  • @Repository:数据访问层,也称为持久层,负责数据访问操作
  • @Configuration:配置层,处理项目中的⼀些配置信息

3.2 方法注解@Bean

@Bean注解作用在方法上,bean的名称就为方法名,在使用方法注解时必须配合使用类注解

java 复制代码
@RestController
public class TestController {

    @Bean
    public Student stu () {
        Student student = new Student();
        student.setId(1);
        student.setAge(18);
        student.setName("sans");
        return student;
    }
}

获取bean:

java 复制代码
@SpringBootApplication
public class SpringBootDemo1Application {

	public static void main(String[] args) {
		//1. 获取Spring上下文对象
		ApplicationContext context = SpringApplication.run(SpringBootDemo1Application.class, args);
		//2. 从Spring上下文中获取想要的对象
		Student student = context.getBean(Student.class);

		System.out.println(student.toString());
    }
}

3.2.1 定义多个对象

java 复制代码
@RestController
public class TestController {

    @Bean
    public Student stu1 () {
        Student student = new Student();
        student.setId(1);
        student.setAge(18);
        student.setName("sans");
        return student;
    }

    @Bean
    public Student stu2 () {
        Student student = new Student();
        student.setId(2);
        student.setAge(19);
        student.setName("frisk");
        return student;
    }
}

通过bean的名称获取bean:

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
    public static void main(String[] args) {
        //获取Spring上下⽂对象
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        //根据bean名称, 从Spring上下⽂中获取对象
        Student stu1 = (Student) context.getBean("stu1");
        Student stu2= (Student) context.getBean("stu2");
        System.out.println(stu1);
        System.out.println(stu2);
    }
}

3.2.2 重命名Bean

起别名可以起多个

java 复制代码
@RestController
public class TestController {

    @Bean(name = {"stu1", "s1"}) // 此时bean名为stu1或者s1
    public Student stu () {
        Student student = new Student();
        student.setId(1);
        student.setAge(18);
        student.setName("sans");
        return student;
    }
}

如果只起一个别名,可以省略 name = {},直接写成 @Bean(s1)

3.3 扫描路径

bean想要生效就需要被spring扫描,spring会默认扫描启动类所在的包及其子包,假如把启动类放到service层

此时再从spring容器中获取bean,就会显示无法获取名为stu1的bean

Spring也提供了一个注解可以配置扫描路径→@ComponentScan

四、DI详解

前面讲过,依赖注入就是将依赖对象注入到其要使用的对象

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

  • 属性注入
  • 构造方法注入
  • Setter注入

4.1 属性注入

接下来,我们将Service类注入到Controller类

Service类如下:

java 复制代码
@Service
public class UserService {
    public void sayHi () {
        System.out.println("Hello UserService");
    }
}

Controller类如下:

java 复制代码
@RestController
public class UserController {

    //属性注入
    @Autowired 
    private UserService userService;
    
    public void sayHi () {
        System.out.println("hello Controller");
        userService.sayHi();
    }

}

接下来从Spring中获取Controller对象,并执行sayHi方法,查看是否能执行成功

java 复制代码
@SpringBootApplication
public class SpringBootDemo1Application {

	public static void main(String[] args) {
		//1. 获取Spring上下文对象
		ApplicationContext context = SpringApplication.run(SpringBootDemo1Application.class, args);
		//2. 从Spring上下文中获取想要的对象
		UserController userController = context.getBean(UserController.class);
        //3. 使用对象
		userController.sayHi();
    }
}

可以看到执行成功,说明Service对象注入成功

4.2 构造方法注入

java 复制代码
@Controller
public class UserController2 {
    
    private UserService userService;

    //构造⽅法注入
    @Autowired
    public UserController2(UserService userService) {
        this.userService = userService;
    }

    public void sayHi(){
        System.out.println("hi,UserController2...");
        userService.sayHi();
    }
}

如果类中只有一个构造方法,则可以不加@Autowired,如果有多个构造方法,则需要加上@Autowired来明确要使用哪个构造方法

4.3 Setter注入

在对象的set方法上加上@Autowired

java 复制代码
@Controller
public class UserController3 {
    //注⼊⽅法3: Setter⽅法注⼊
    private UserService userService;
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    public void sayHi(){
        System.out.println("hi,UserController3...");
        userService.sayHi();
    }
}

4.4 三种注入方式的区别

属性注入:只能用于IoC容器,如果是IoC容器则不能用,不能注入final修饰的属性

构造方法注入:

  • 可以注入final修饰的属性
  • 注入的对象不会被修改
  • 依赖对象在使用前一定会被完全初始化,因为依赖是在类的构造方法中执行的,而构造方法是在类加载时执行的
  • 注入多个对象时,代码会比较繁琐

Setter注入:

  • 不能注⼊⼀个Final修饰的属性
  • 注⼊对象可能会被改变,因为setter⽅法可能会被多次调⽤,就有被修改的⻛险

4.5 @Autowired存在的问题

当同一个类型存在多个bean时,@Autowired就会出现问题

下面代码中,有两个User类型的bean:u1 和 user2

java 复制代码
@Component
public class BeanConfig {
    @Bean("u1")
    public User user1(){
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
    @Bean
    public User user2() {
        User user = new User();
        user.setName("lisi");
        user.setAge(19);
        return user;
    }
}

接下来将User类注入到Controller类中:

java 复制代码
@RestController
public class UserController {

    @Autowired
    private UserService userService;
    //注⼊user
    @Autowired
    private User user;

    public void sayHi(){
        System.out.println("hi,UserController...");
        userService.sayHi();
        System.out.println(user);
    }
}

结果如下:

该原因是:有两个User类型的bean:u1 和 user2,在向Controller类注入User对象时,程序不知道该注入哪个,因此出现了上述错误

解决方案:

  • @Primary
  • @Qualifier
  • @Resource

@Primary:确定默认注入的bean

java 复制代码
@Primary //指定该bean为默认bean的实现
@Bean("u1")
public User user1(){
    User user = new User();
    user.setName("zhangsan");
    user.setAge(18);
    return user;
}

@Qualifier:需要配合@Autowired使用,指定当前要注入bean的名称

java 复制代码
@Qualifier("user2") //指定bean名称
@Autowired
private User user;

public void sayHi(){
    System.out.println("hi,UserController...");
    System.out.println(user);
}

@Resource:按照bean的名称注入,通过name属性指定要注入的bean

java 复制代码
@Resource(name = "user2")
private User user;

public void sayHi(){
    System.out.println("hi,UserController...");
    System.out.println(user);
}

【常见面试题】@Autowired vs @Resource

  • @Autowired是spring框架提供的注解,而@Resource是JDK提供的注解
  • @Autowired默认是按照类型注⼊,而@Resource是按照名称注⼊,相比于@Autowired来说,@Resource⽀持更多的参数设置,例如name设置,根据名称获取Bean。

🙉本篇文章到此结束

相关推荐
P7进阶路16 分钟前
Tomcat异常日志中文乱码怎么解决
java·tomcat·firefox
小丁爱养花1 小时前
Spring MVC:HTTP 请求的参数传递2.0
java·后端·spring
CodeClimb1 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
等一场春雨1 小时前
Java设计模式 九 桥接模式 (Bridge Pattern)
java·设计模式·桥接模式
带刺的坐椅1 小时前
[Java] Solon 框架的三大核心组件之一插件扩展体系
java·ioc·solon·plugin·aop·handler
不惑_2 小时前
深度学习 · 手撕 DeepLearning4J ,用Java实现手写数字识别 (附UI效果展示)
java·深度学习·ui
费曼乐园2 小时前
Kafka中bin目录下面kafka-run-class.sh脚本中的JAVA_HOME
java·kafka
feilieren3 小时前
SpringBoot 搭建 SSE
java·spring boot·spring
阿岳3163 小时前
Java导出通过Word模板导出docx文件并通过QQ邮箱发送
java·开发语言
Amor风信子3 小时前
华为OD机试真题---战场索敌
java·开发语言·算法·华为od·华为