Spring IoC&DI

一,Spring IoC&DI 的定义

我们知道Spring是一个开源框架,简单来说就是一个包含众多工具的IoC容器。容器都知道是用来装东西的,那么什么是IoC?IoC是Spring的核心思想之一,在上一篇SpringMVC的博客中也使用到了,即类的注解,比如@RestController,@Controller注解,它们就表示将这个对象交给Spring管理,Spring框架启动时就会加载该类,把对象交给Spring管理,就是IoC思想。

1.1 IoC——控制反转

IoC,全称 Inversion of Control(控制反转),就是将控制权反转,再进一步来说就是将获得依赖对象的过程反转。IoC思想的主要目的就是降低代码的耦合度。比如说,做一件事,要先做...,再做...,最后做,,,,每一个操作要依赖于前一个操作,而Spring IoC容器就是用来管理这些操作,使我们不必关心前一个操作是否完成,而是只关注于本身的实现。

举个例子,如果不使用IoC思想:

public class demo1 {
    public static void main(String[] args) {
        Car car = new Car();
    }
    static class Car{
        private  Framework framework;
        public Car(){
            framework = new Framework();
            System.out.println("初始化car");
        }
    }

    static class Framework{
        private Bottom bottom;
        public Framework(){
            bottom = new Bottom();
            System.out.println("初始化framework");
        }
    }

    static class Bottom{
        public Bottom(){
            System.out.println("初始化bottom");
        }
    }
}

如果我们要改变bottom的大小,就需要把所有的代码都修改,即代码耦合度太高:

public class demo1 {
    public static void main(String[] args) {
        int size = 100;
        Car car = new Car(size);
    }
    static class Car{
        private  Framework framework;
        public Car(int size){
            framework = new Framework(size);
            System.out.println("初始化car");
        }
    }

    static class Framework{
        private Bottom bottom;
        public Framework(int size){
            bottom = new Bottom(size);
            System.out.println("初始化framework");
        }
    }

    static class Bottom{
        public Bottom(int size){
            System.out.println("初始化bottom");
        }
    }
}

1.2 DI——依赖注入

依赖注入就是实现控制反转的一种方式,我们不在每一个类中自己创建需要依赖的类,而是通过传递参数的形式来获得依赖的类(这里指自己实现,Spring中有专门的注解来帮我们实现,下面会讲),这种获得依赖的方式就称为DI(依赖注入),这样就实现了程序的解耦。

举个例子,将上述程序使用DI的方式实现:

public class demo1 {
    public static void main(String[] args) {
        int size = 100;
        Bottom bottom = new Bottom(size);
        Framework framework = new Framework(bottom);
        Car car = new Car(framework);
    }
    static class Car{
        private Framework framework;
        public Car(Framework framework){
            this.framework = framework;
            System.out.println("初始化car");
        }
    }

    static class Framework{
        private Bottom bottom;
        public Framework(Bottom bottom){
            this.bottom = bottom;
            System.out.println("初始化framework");
        }
    }

    static class Bottom{
        private int size;
        public Bottom(int size){
            System.out.println("初始化bottom");
        }
    }
}

这样写的话,之后底层类无论如何变化,整个调用链都不用做出任何改变,这样就实现了代码之间的解耦。

1.3 IoC容器的优势

  • 原本代码中类的创建顺序是:Car -> Framework -> Bottom
  • 使用DI后代码中类的创建顺序是:Bottom -> Framework -> Car

可以发现改进后的代码,它创建类的顺序是反的,也是我们之前说的控制权发生了反转,不再是使用法对象创建并控制依赖对象了,而是把依赖对象注入到当前对象中,依赖对象的控制权不再由当前类控制了,这样的话无论依赖类发生任何改变,当前类都是不受影响的,这就是为什么我们将其称之为"控制反转",这也是IoC的实现思想。

这里我们就彻底了解了上面所说的IoC思想,那么这里的IoC容器究竟有什么作用呢?

Bottom bottom = new Bottom(size);
Framework framework = new Framework(bottom);
Car car = new Car(framework);

上述代码就是IoC容器所做的工作,也就是说我们将需要使用的资源交给不使用资源的第三方管理,这可以带来很多好处:

  1. 实现了资源的集中管理:IoC容器会帮我们管理一些对象,当我们需要使用时,直接去IoC容器中取就行了
  2. 创建实例时不需要了解其中的细节,降低了耦合度

二,IoC 详解

既然Spring是一个IoC(控制反转)容器,作为容器,那么它就具备两个基本功能:存储,获取。Spring容器管理的主要是对象,这些对象,我们称之为"Bean",我们将这些对象交给Spring管理,我们程序只要告诉Spring,哪些存,以及如何从Spring中取出对象。

2.1 Bean的存储

要把某个对象交给IoC容器管理,需要在类或方法上添加一个注解:

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

接下来分别来看

2.1.1 @Controller(控制器存储)

@Controller//将对象存储到Spring容器中
public class UserController {
    public void run(){
        System.out.println("UserController running...");
    }
}

为了观察这个对象是否存储到Spring中,我们需要从Spring中获取对象,代码如下:

@SpringBootApplication
public class JavaEeSpringIoCApplication {

    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context = SpringApplication.run(JavaEeSpringIoCApplication.class, args);
        //获取对象
        UserController userController = context.getBean(UserController.class);
        //使用对象
        userController.run();
    }
}

这里出现了一个Spring上下文,上下文就是指当前的运行环境,我们可以把它当作一个容器,容器中存储的就是当前的运行环境。

观察运行结果,发现成功从Spring中获取到UserController对象,并执行了它的run方法:

2.1.2 获取Bean的方式

上述代码是根据类型来查找对象,如果Spring容器中,同一个类存在多个Bean(即对象)的话,我们就需要通过其他方式获取,这里介绍三种常用的方法:

public interface BeanFactory {
    //以上省略...

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

    //以下省略...
}

前两种方法都涉及到根据名称来获取对象,那么bean的名称是什么呢?Spring bean是Spring框架再运行时管理的对象,Spring会给管理的对象取一个名字,根据Bean的名称就可以获取到对应的对象。它的底层代码是这样的:

    //如果name前两个字符是大写,那么直接返回name
    //否则将name的第一个字符小写后,返回name
    public static String decapitalize(String name) {
        if (name == null || name.length() == 0) {
            return name;
        }
        if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
                        Character.isUpperCase(name.charAt(0))){
            return name;
        }
        char chars[] = name.toCharArray();
        chars[0] = Character.toLowerCase(chars[0]);
        return new String(chars);
    }

演示一下上述三种方法:

@SpringBootApplication
public class JavaEeSpringIoCApplication {

    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context = SpringApplication.run(JavaEeSpringIoCApplication.class, args);
        //获取对象
        UserController userController1 = (UserController) context.getBean("userController");
        UserController userController2 = context.getBean("userController", UserController.class);
        UserController userController3 = context.getBean(UserController.class);
        //使用对象
        System.out.println(userController1);
        System.out.println(userController2);
        System.out.println(userController3);
    }
}

2.1.3 @Service(服务存储)

@Service
public class UserService {
    public void run(){
        System.out.println("userService run...");
    }
}

获取该对象:

@SpringBootApplication
public class JavaEeSpringIoCApplication {

    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context = SpringApplication.run(JavaEeSpringIoCApplication.class, args);
        //获取对象
        UserService userService = context.getBean(UserService.class);
        //使用对象
        userService.run();
    }
}

2.1.4 @Repository(仓库存储)

@Repository
public class UserRepository {
    public void run(){
        System.out.println("UserRepository run...");
    }
}

获取该对象:

@SpringBootApplication
public class JavaEeSpringIoCApplication {

    public static void main(String[] args) {
        //获取Spring上下文
        ApplicationContext context = SpringApplication.run(JavaEeSpringIoCApplication.class, args);
        //获取对象
        UserRepository userRepository = context.getBean(UserRepository.class);
        //使用对象
        userRepository.run();
    }
}

2.1.5 @Component(组件存储)

@Component
public class UserComponent {
    public void run(){
        System.out.println("UserComponent run...");
    }
}

2.1.6 @Configuration(配置存储)

@Configuration
public class UserConfiguration {
    public void run(){
        System.out.println("UserConfiguration run...");
    }
}

获取与上面的做法一样,这里就不做过多的赘述。

2.2 五大注释的区别与作用

之所以有这么多注释就是为了让程序员区分被该注释的类的用途:

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

通过源码来看一下类注解之间的关系:

//@Component源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}

//@Repository源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

//@Configuration源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";

    boolean proxyBeanMethods() default true;
}

//@Controller源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

//@Service源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

可以发现@Controller / @Service / @Repository / @Configuration这些注解中都有一个@Component注解,说明它们本身就属于@Component的子类,@Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service , @Repository 等. 这些注解被称为 @Component 的衍⽣注解,@Component也是Spring能够自动识别并管理类的实例的原因。

下图可以看出它们之间的关系

2.3 方法注释@Bean

为什么还需要有一个方法注释?

  1. 如果要使用第三方库中的类,就没法自主添加类注释
  2. 一个类可能会创建多个对象,使用类注释无法实现

2.3.1 定义一个对象

@Component
public class UserBean {
    @Bean
    public User user(){
        User user = new User(18,"zhangsan");
        return user;
    }
}

注意:@Bean注解必须搭配类注解一起使用!!!,不然不会存储到Spring容器中

2.3.2 定义多个对象

@Component
public class UserBean {
    @Bean
    public User user1(){
        User user = new User(18,"zhangsan");
        return user;
    }
    @Bean
    public User user2(){
        User user = new User(10,"lisi");
        return user;
    }
}

当一个类出现多个对象时,我们就不能使用类来获取对象,不然会出现下面的报错:

这时就必须要通过它们的名字来获取对象,注意@Bean注释定义的对象,默认名称就是方法名,不需要额外去判断修改,我们可以通过名称获取:

当然我们也可以通过name属性来重命名:

@Component
public class UserBean {
    @Bean({"UserInfo"})//可以取多个名称
    public User user(){
        User user = new User(18,"zhangsan");
        return user;
    }
}

2.4 扫描路径

想要将对象交给Spring管理,不仅需要添加类注解,方法注解,还需要让这些类在Spring的扫描路径下。

这时我们可以通过@ComponentScan()注解来配置Spring的扫描路径:

注:{}中可以配置多个路径

三,DI详解

在Spring中,我们可以使用@Autowired注解来完成依赖注入的操作,Spring提供了三种方式:

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

3.1 属性注入

注入多个对象时,每个对象都要添加@Autowired注解

@Controller
public class UserController {
    @Autowired
    private UserService userService;

    @Autowired 
    private UserConfiguration userConfiguration

    public void run(){
        userService.run();
        System.out.println("UserController running...");
    }
}

3.2 构造方法注入

当只有一个构造方法,@Autowired注解可以不加

@Controller
public class UserController {
    private UserService userService;
    @Autowired
    public UserController(UserService userService){
        this.userService = userService;
    }
    public void run(){
        userService.run();
        System.out.println("UserController running...");
    }
}

当有多个构造方法,如果有@Autowired注解,那么就会调用有注解的那个;如果没有@Autowired注解,那么就会默认调用无参的构造方法。

@Controller
public class UserController {
    private UserService userService;
    private UserRepository userRepository;
    public UserController(){}
    public UserController(UserService userService){
        this.userService = userService;
    }
    @Autowired
    public UserController(UserService userService, UserRepository userRepository){
        this.userService = userService;
        this.userRepository = userRepository;
    }
    public void run(){
        userService.run();
        userRepository.run();
        System.out.println("UserController running...");
    }
}

3.3 Setter注入

@Controller(value = "u1")//可以通过value属性重命名
public class UserController {
    private UserService userService;
    private UserRepository userRepository;
    @Autowired
    public void setUserService(UserService userService){
        this.userService = userService;
    }
    @Autowired
    public void setUserRepository(UserRepository userRepository){
        this.userRepository = userRepository;
    }

    public void run(){
        userService.run();
        userRepository.run();
        System.out.println("UserController running...");
    }
}

3.4 @Autowired存在的问题

@Autowired注解会先根据类来查找需要注入的对象,如果该类有多个对象,就会根据名称来选择注入的对象,但是不推荐这种用法(因为在公司中一个项目通常是由多个人完成,默认变量名的修改不会影响业务的实现):

为了避免上述做法,Spring提供了以下几种解决方法:@Primarily,@Qualifier,@Resource

  • @Primarily:可以在其中一个bean上添加@Primary注解,以指示Spring容器在存在多个候选者时应该优先考虑哪一个bean。如果有且仅有一个bean被标记为@Primary,则Spring将使用该bean进行注入。
  • @Component
    public class UserBean {
        @Bean
        @Primary
        public User user1(){
            User user = new User(18,"zhangsan");
            return user;
        }
        @Bean
        public User user2(){
            User user = new User(10,"lisi");
            return user;
        }
    }
  • @Qualifier:如果@Primary不足以解决问题(例如,如果有多个@Primary标记的bean,或者想基于不同的条件注入不同的bean),可以使用@Qualifier注解来明确指定要注入的bean的名称。@Qualifier可以与@Autowired一起使用,或者与@Bean方法一起使用在配置类中
  • @Repository
    public class UserRepository {
        @Autowired
        @Qualifier("user1")
        private User user;
        public void run(){
            System.out.println("UserRepository run...");
        }
    }
  • @Resource:是按照bean的名称进⾏注⼊,通过name属性指定要注⼊的bean的名称。

  • @Repository
    public class UserRepository {
        @Resource(name = "user1")
        private User user;
        public void run(){
            System.out.println(user);
            System.out.println("UserRepository run...");
        }
    }

四,常见面试题

4.1 三种注入方式的优缺点

1.属性注入

  • 优点:简洁方便
  • 缺点:只能在IoC容器中使用;不能注入一个final修饰的属性

2.构造方法注入:

  • 优点:可以注入final修饰的属性;通用性好,是JDK支持的,可以任用于其他框架;注入对象不会被修改;依赖对象在使⽤前⼀定会被完全初始化,因为构造方法在类加载阶段就会执行
  • 缺点:代码比较繁琐

3.Setter注入:

  • 优点:方便在类实例之后, 重新对该对象进行配置或者注入
  • 缺点:不能注入一个Final修饰的属性;注入对象可能会被改变, 因为setter方法可能会被多次调用, 就有被修改的风险

4.2 @Autowired与@Resource的区别

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

4.3  ApplicationContext与BeanFactory

  • BeanFactory 提供了基础的访问容器的能力,ApplicationContext是BeanFactory的子类,继承了BeanFactory的所有功能之外,还有独特的功能,比如:支持国际化,支持资源访问。
  • ApplicationContext是一次性加载并初始化所有Bean对象,而BeanFactory是用到哪个才去加载哪个,所以BeanFactory比ApplicationContext更加轻量。(空间换时间)
  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一叶祇秋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值