【Spring IoC】带你了解Spring控制反转的魅力

Spring IoC介绍

Spring IoC 与 AOP 作为Spring两个核心思想,学好这两大知识是对掌握Spring是非常重要的。

IoC全名为Inversion of Control,即“控制反转”,何为控制反转呢?

  • 控制:指的是对象的创建、初始化、销毁和生命周期的管理权

  • 反转:指这种管理权发生了反转:

    • 传统模式:一般是由程序员来手动new创建对象、并管理依赖,类似UserController user = new UserController()
    • IoC模式:对象的创建和依赖的注入权交给Spring来管理。程序不再由程序员主动来创建,而是被动等待Spring容器将所需的对象注入 / 提取出来

SpringIoC作为一个容器,它管理(装)的是对象(Bean),通过注解将对象交给Spring管理,有两类注解:

  • 类注解:@Controller @Service @Repository @Configuration @Component
  • 方法注解:@Bean

Q:为什么需要这种模式?

A:目的是为了解耦合,将对象的创建权交给Spring,各个组件之间不再强依赖,使得代码更加容易维护、管理与扩展

这里有简单的代码实力,对比传统模式与IoC模式的区别:假设我们有一个用户服务 (UserService) ,它需要调用数据访问层 (UserDao) 来把用户信息保存到数据库

  1. 传统模式:在这种模式下,UserService自己负责创建它需要的依赖对象。

    public class UserService {
        // ❌ 缺点:高度耦合
        // UserService 必须明确知道 UserDao 的具体实现类是哪个 (UserDaoImpl)
        // 如果将来想换成 UserDaoNewImpl,必须修改这里的代码
        private UserDao userDao = new UserDaoImpl();
    
        public void saveUser(String name) {
            userDao.add(name);
        }
    }
    
    • 控制权: 在程序员手中。你必须手动写 new UserDaoImpl()
    • 问题: 代码“写死”了。如果你想做单元测试(Mock 一个假的 UserDao),非常困难。
  2. Spring IoC模式(依赖注入):在这种模式下,UserService只定义它需要什么,而不关心对象是怎么来的。Spring 容器负责把对象“递”进去。

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service // 1. 告诉 Spring:这个类交给你管理,它是一个 Bean(对象)
    public class UserService {
    
        // ✅ 优点:解耦
        // UserService 只知道它需要一个 UserDao 接口
        // 具体是哪个实现类,由 Spring 容器自动注入
        @Autowired 
        private UserDao userDao;
    
        public void saveUser(String name) {
            userDao.add(name);
        }
    }
    
    • 控制权: 在 Spring 容器手中(反转了)。

    • 发生了什么:

      1. Spring 扫描到 @Service​,创建并管理了 UserService 的实例。
      2. Spring 看到 @Autowired​,发现 UserService​ 需要一个 UserDao
      3. Spring 在自己的容器里找到合适的 UserDao​ 实例,直接注入(赋值)给 userDao 变量。
    • 前提是UserDao​也需要被Spring管理,数据层一般添加@Repository注解管理

      import org.springframework.stereotype.Repository;
      
      // 1. 定义接口
      public interface UserDao {
          void add(String name);
      }
      
      // 2. 实现接口,并加上 @Repository 注解
      @Repository 
      public class UserDaoImpl implements UserDao {
          
          @Override
          public void add(String name) {
              // 模拟数据库操作
              System.out.println("保存用户到数据库: " + name);
          }
      }
      
      • 看到这会疑惑为什么要专门写一个UserDao​接口,然后专门交给UserDaoImpl来实现?

      • 简短的回答是:

        从技术上讲,完全可以不写接口,直接定义一个 UserDao​ 类并加上 @Repository,Spring 照样能工作

        但是使用接口+实现类的模式主要有以下优点:

        1. 这是面向对象设计(SOLID 原则)中的 D (Dependency Inversion Principle)思想——上层模块(Service)不应该依赖于下层模块(DAO)的具体实现,而应该依赖于抽象(接口)。

          UserService​只需要知道有UserDao​接口,只需要调用UserDao接口内的方法就好了,但具体怎么实现的则不需要知道(是用 MySQL 写的,还是用 Oracle 写的,或者是把数据写到了 Redis 里)

          举例: 如果有一天你需要把数据库从 MySQL 换成 MongoDB,你只需要新写一个 UserDaoMongoImpl​ 实现接口,而不需要修改 UserService 的任何一行代码。

        2. Spring AOP的原因

          AOP的底层原理是动态管理

          • JDK动态代理(标准)

            只能代理接口,如果Bean对象实现了接口,Spring会默认使用JDK动态代理,效率高,原生支持好

          • CGLIB代理(第三方)

            如果Bean对象没有实现接口,只是代理了一个普通的类,Spring就必须使用第三方CGLIB库,继承你的类并重写方法来生成代理

          虽然现在SpringBoot能很好的解决这一类问题,但使用接口配合JDK动态管理已经成为了一种惯例

        3. 💫ORM框架的特殊需求(如MyBatis、JPA)

          如果使用的是MyBatis或Spring Data JPA,那就只需要写接口,连实现类都不用写

          • MyBatis:你定义UserMapper接口,MyBatis会自动生成代理对象执行SQL
          • JPA:你定义UserRepository​接口,继承JpaRepository,Spring Data自动帮你生成增删改查的实现

          在这种情况下,实现接口是必须的,因为实现类是由框架在运行时动态生成的,你也无法去写一个具体的类

五大注解详解

上面粗略讲到了这些注解,接下来就来详细分析以下

  • 类注解:@Controller @Service @Repository @Configuration @Component
  • 方法注解:@Bean

来着重讲一下类注解,能发现这有五种不同的类注解,但都能交给Spring管理,那他们的区别是什么呢?

不同的类注解从名字上就能看出主要应用在哪个层,最基本的项目开发都会有 “控制层—>服务逻辑层—>数据层” 的结构,如果更加复杂的项目开发分开的更加细致,所以不同的注解名字也能第一眼就能看出这个对象主要在哪个层在工作

基本关系为: 控制层调用服务层,服务层调用数据层,数据层返回结果给服务层,服务层再返回结果给控制层。

@Controller:控制层——接受参数,返回响应(前后端交互的入口)

@Service:服务层

@Repository:业务逻辑层

@Configuration:配置层

@Component:组件层——除此以外的所有需要交给Spring管理的对象都能用此注解

我们扒开源码查看,能看出他们之间的关系

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

其中@Controller @Service @Repository @Configuration 都实现了@Component,它们都是@Component的衍生注解

在实际应用中,这五个注解的边界并没有这么清晰,但控制层必须使用@Controller,不可以和其他注解替换

BeanName

在Spring容器中取出对象时会使用对应的BeanName,类注解与方法注解都有不同的BeanName命名方法

  1. 五大类注解

    BeanName默认为类名的小驼峰写法,如果类名前两位字母均为大写则BeanName为类名本身,举例:

    @Controller
    public class UserController {
        public void print() {
            System.out.println("do controller");
        }
    }
    
    public static void main(String[] args) {
        /**
         * 获取 bean 的命名方式
         * 首字母需变小写
         * 如果连续两个大写字母开头则直接写
         */
        ApplicationContext run = SpringApplication.run(SpringIocApplication.class, args);
    	UserController bean = (UserController) run.getBean("userController");
    	bean.print();
    }
    
    1. 方法注解@Bean

      需要搭配五大注解使用,BeanName为默认方法名,举例:

      @Component
      public class StudentComponent {
          @Bean
          public Student s1 () {
              return new Student("lili",9);
          }
          @Bean
          public Student s2 () {
              return new Student("Jack",25);
          }
      }
      
      public static void main(String[] args) {
      	/**
           * 同时在注解中也提供了对类 / 方法进行重命名
           * 包括 @Controller @Service @Configuration @Repository @Component @Bean
           * 注解后对应的类名 / 方法名则无效
           * 只能填重命名后的
           * @Bean("aaa")
           *     public Student s2 () {
           *         return new Student("Jack",25);
           *     }
           * 如果是run.getBean("s2") 报错:NoSuchBeanDefinitionException
           */
          Student s = (Student)run.getBean("s2");
          System.out.println(s);
      }
      

希望看到这里对你有所帮助,如有错误欢迎指出,祝愿各位身体健康

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值