初始JavaEE篇 —— Spring IOC 和 DI

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程程(ಥ_ಥ)-CSDN博客

所属专栏:JavaEE

目录

应用分层 

IOC 和 DI 的简单使用

IOC详解

Bean的存储

通过@Controller存储

通过@Service存储

通过@Repository存储 

通过@Configuration存储

通过@Component存储 

方法注解@Bean

重命名Bean 

扫描路径

DI详解

属性注入

构造方法注入

setter方法注入

依赖注入三种方式的优劣势

@Autowired注解的缺陷


应用分层 

初始JavaEE篇 —— Spring Web MVC综合练习-CSDN博客

经过前面的学习,我们已经掌握了如何编写简单的前后端程序了。但是我们编写的程序都是放在了一个包下面,非常的混乱。接下来,我们就来学习应用分层。

应用分层的概念:应用分层是一种软件开发设计思想,它将应用程序分成N个层次这,N个层次分别负责各自的职责,多个层次之间协同提供完整的功能。根据项目的复杂度,把项目分成三层,四层或者更多层。前面学习的MVC就是应用分层的一种具体实现。

那为什么需要应用分层呢?当我们写的代码比较简单时,是无需进行分层的,但随着后面不断增加功能,代码也是越来越多,如果不分层的话,就会出现逻辑不清晰、代码扩展性差、改动一处,就会牵一发而动全身。

既然分层这么重要,那如何进行分层呢?由于现在开发方式是"前后端分离开发",因此后端开发人员并不需要关注前端的实现,整体的分层架构也和MVC不一致了。虽然还是三层架构,但是这三层全部是针对后端了:表现层(控制层)、业务逻辑层、数据层。

表现层(控制层):接收前端发送的请求,处理参数,并返回响应。

业务逻辑层:实现具体的业务逻辑。

数据层:负责和数据库进行交互,实现数据的增删改查。

我们就以下面的代码为例子,进行应用分层:

@RestController
@RequestMapping("/book")
public class BookController {
    @RequestMapping("/getList")
    public List<BookInfo> getList() {
        // 由于并未学习数据库相关操作,因此我们这里是直接mock数据
        List<BookInfo> ret = DataMock();
        // 对数据进行二次处理(将中文状态设置一下)
        for (BookInfo bookInfo : ret) {
            if (bookInfo.getBookStatues() == 1) {
                bookInfo.setBookStatusCN("可借阅");
            } else {
                bookInfo.setBookStatusCN("不可借阅");
            }
        }
        return ret;
    }

    private List<BookInfo> DataMock() {
        // 随机生成一些数据即可
        List<BookInfo> ret = new ArrayList<>();
        for (int i = 1; i <= 15; i++) {
            BookInfo bookInfo = new BookInfo();
            bookInfo.setBookId(i);
            bookInfo.setBookName("余华的第"+i+"本书");
            bookInfo.setBookAuthor("余华");
            // 设置数量区间为[20, 99]
            bookInfo.setBookNum(new Random().nextInt(80)+20);
            // 设置价格区间为[20, 49]
            bookInfo.setBookPrice(new BigDecimal(new Random().nextInt(30)+20));
            bookInfo.setBookPublish("人民出版社");
            bookInfo.setBookStatues(new Random().nextInt(2));
            ret.add(bookInfo);
        }
        return ret;
    }
}

我们需要再主包下面创建四个包:controller包、service包、dao包、pojo包。controller包是存放控制层的代码,service包是存放处理业务逻辑的代码,dao包是存放存取数据的代码,pojo包是存放实体类的代码。注意:存放实体类的包,可以取 pojo、entity、model 这三个名字,区别不大。

最终经过我们一系列修改上述代码可以修改成下面这样:

BookController:

@RestController
@RequestMapping("/book")
public class BookController {
    // 引入Service来处理业务逻辑
    private BookService bookService = new BookService();

    @RequestMapping("/getList")
    public List<BookInfo> getList() {
        // 只需接收参数和返回结果
        return bookService.getBook();
    }
}

BookService:

public class BookService {
    // 引入Dao来获取数据
    private BookMockDao bookMockDao = new BookMockDao();

    public List<BookInfo> getBook() {
        // 获取从数据库中查询的数据
        List<BookInfo> ret = bookMockDao.DataMock();
        // 对数据进行二次处理(将中文状态设置一下)
        for (BookInfo bookInfo : ret) {
            if (bookInfo.getBookStatues() == 1) {
                bookInfo.setBookStatusCN("可借阅");
            } else {
                bookInfo.setBookStatusCN("不可借阅");
            }
        }
        return ret;
    }
}

BookDao:

public class BookDao {
    public List<BookInfo> DataMock() {
        // 随机生成一些数据即可
        List<BookInfo> ret = new ArrayList<>();
        for (int i = 1; i <= 15; i++) {
            BookInfo bookInfo = new BookInfo();
            bookInfo.setBookId(i);
            bookInfo.setBookName("余华的第"+i+"本书");
            bookInfo.setBookAuthor("余华");
            // 设置数量区间为[20, 99]
            bookInfo.setBookNum(new Random().nextInt(80)+20);
            // 设置价格区间为[20, 49]
            bookInfo.setBookPrice(new BigDecimal(new Random().nextInt(30)+20));
            bookInfo.setBookPublish("人民出版社");
            bookInfo.setBookStatues(new Random().nextInt(2));
            ret.add(bookInfo);
        }
        return ret;
    }
}

经过上述分层处理,代码的逻辑变得非常清晰,后续修改的话,也不用大块的修改了,只需要将功能代码块修改,而且并不会影响到别的代码块了。上述的处理符合了高内聚的特点,每个方法的功能都是单一的,功能并不混杂。而软件的设计原则除了高内聚之外,还有低耦合。但是上述代码并未体现低耦合的特性,反而代码块之间的耦合性比较高。

代码之间的耦合度越高,后期我们想要修改代码的成本也就越大,这是我们不想看到的。

可能有小伙伴会疑惑:Controller层的代码本身就依赖于Service层的代码,而Service层的代码本身就依赖于Dao层的代码。这个关系是不可避免的呀!因为Service层的代码和Dao层的代码本质上是从Controller层的代码上剥离出来的呀!

确实,上面的说法是正确的,但是我们有没有什么办法可以降低耦合度呢?这就是接下来我们需要学习的Spring IOC 和 DI。

IOC 和 DI 的简单使用

我们先对上述代码进行改进:

只需要修改上述几个地方,就可以达到和之前一样的效果。 这就是IOC和DI,IOC的全称是 Inversion of control(控制反转),DI的全称是 Dependency Injection(依赖注入)。那两者是什么意思呢?控制反转,是指对象的创建权由程序交给了Spring,之前对象的创建时机是由程序决定的,而现在对象的创建时机是由Spring决定的了,这就是对象的创建权(不仅仅是创建权,但这里只能看出创建权)发生了反转。依赖注入,是指对象的创建不再需要去new了,而是Spring在程序运行时,会自动给其分配,将程序所运行的依赖注入到了程序中(有点像多态),即依赖注入。

@Controller 和 @Component 都是实现控制反转的注解,而@Autowired 则是实现依赖注入的注解。控制反转和依赖注入的两个基本注解:

注解 作用
@Component 属于类注解,用于将类标记为 Spring 管理的 Bean,使其被 IOC 容器扫描并实例化
@Autowired

属于属性、构造方法、Setter 方法、普通方法注解,在程序运行时,将会在容器中去找对应类型的对象并赋值。

而 @Controller的底层实际上也是@Component,因此@Controller也可以实现控制反转的效果。知道了IOC和DI的基本使用,现在我们就来学习其原理。

IOC详解

IOC是指,对象的控制权发生反转,由程序转变为Spring了,这里涉及了Bean(对象)的存储。

Bean的存储

虽然Bean的存储的注解有很多,但是Spring为了实现分层的效果,提供了两大类注解:类注解 和 方法注解。而类注解又分为:@Controller、@Service、@Repository、@Configuration、@Component,先来学习类注解。

通过@Controller存储

先将对象通过@Controller存储在Spring容器中:

@Controller
public class HelloController {
    public void print() {
        System.out.println("do HelloController");
    }
}

 那如何知道这个对象存储在容器内部呢?可以通过Spring的上下文来获取对象:

@SpringBootApplication // 启动类注解,表明当前类是一个启动类
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文对象
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        // 我们可以通过Spring的上下文来获取容器中的对象
        HelloController bean = context.getBean(HelloController.class);
        bean.print();
    }

}

我们可以启动该程序,最终会在控制台上打印出:

上述结果表明:HelloController对象确实被Spring存储在容器中。

从上图的结果看出:run方法的返回值,确实可以使用 ApplicationContext 类来接收,然后我们再使用其父类所提供的 getBean 方法来获取 Bean即可。这里获取 Bean 的方式有很多种,我们只需要学习其中常见的三种即可,如下所示:

代码演示:

// 根据Bean的name获取对象
HelloController bean = (HelloController)context.getBean("helloController");
bean.print();

// 根据 Bean的name 和 类型 来获取对象
HelloController bean2 = context.getBean("helloController", HelloController.class);
bean2.print();

上面的代码都是可以成功运行的,但可能会有小伙伴好奇了:这个Bean的name是怎么来的呢?这个可以根据官方的提供的文档知道,Bean的名称默认为类名的小驼峰写法,但如果类名的前两个字母都是大写的话最终的结果就是类名本身,当然也可以自己去指定相应的名称。下面的写法就符合(小驼峰写法是指类名首字母小写,其余的都是大写):

ClassName                                                BeanName

HelloController                                           helloController(类名的小驼峰写法)

USController                                              USController(类名前两个字母大写,则为类名)

注意:上述获取的对象都是同一个对象:

通过@Service存储

先将对象通过@Service存储到Spring的容器中:

@Service
public class HelloService {
    public void print() {
        System.out.println("do Service");
    }
}

同样也可以使用Spring上下文来获取这个对象:

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        // 我们可以通过Spring的上下文来获取容器中的对象
        // 根据类型获取对象
        HelloService bean1 = context.getBean(HelloService.class);
        bean1.print();

        // 根据Bean的name获取对象
        HelloService bean2 = (HelloService)context.getBean("helloService");
        bean2.print();

        // 根据 Bean的name 和 类型 获取
        HelloService bean3 = context.getBean("helloService", HelloService.class);
        bean3.print();
    }

}

通过@Repository存储 

先将对象通过@Repository存储到Spring容器中:

@Repository
public class HelloRepository {
    public void print() {
        System.out.println("do Repository");
    }
}

再通过Spring上下文来获取对象:

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        // 我们可以通过Spring的上下文来获取容器中的对象
        // 根据类型获取对象
        HelloRepository bean1 = context.getBean(HelloRepository.class);
        bean1.print();

        // 根据Bean的name获取对象
        HelloRepository bean2 = (HelloRepository) context.getBean("helloRepository");
        bean2.print();

        // 根据 Bean的name 和 类型 获取对象
        HelloRepository bean3 = context.getBean("helloRepository", HelloRepository.class);
        bean3.print();
    }

}

通过@Configuration存储

先将对象通过@Configuration存储到Spring容器中:

@Configuration
public class HelloConfiguration {
    public void print() {
        System.out.println("do Configuration");
    }
}

同样还是通过Spring上下文来获取对象:

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        // 我们可以通过Spring的上下文来获取容器中的对象
        // 根据类型来获取
        HelloConfiguration bean1 = context.getBean(HelloConfiguration.class);
        bean1.print();

        // 根据Bean的name来获取
        HelloConfiguration bean2 = (HelloConfiguration)context.getBean("helloConfiguration");
        bean2.print();

        // 根据 Bean的name 和 类型 获取对象
        HelloConfiguration bean3 = context.getBean("helloConfiguration", HelloConfiguration.class);
        bean3.print();
    }

}

通过@Component存储 

先将对象通过@Component存储到对象中:

@Component
public class HelloComponent {
    public void print() {
        System.out.println("do Component");
    }
}

同样还是通过Spring上下文来获取对象:

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        // 我们可以通过Spring的上下文来获取容器中的对象
        // 根据类型获取对象
        HelloComponent bean1 = context.getBean(HelloComponent.class);
        bean1.print();

        // 根据Bean的name来获取对象
        HelloComponent bean2 = (HelloComponent)context.getBean("helloComponent");
        bean2.print();

        // 根据 Bean的name 和 类型 获取对象
        HelloComponent bean3 = context.getBean("helloComponent", HelloComponent.class);
        bean3.print();
    }

}

上述的注解都是类注解, 并且每个注解所处的位置不同的,和应用分层一样,上述注解的作用也是分层的。
 

注解 作用层级
@Controller 控制层
@Service 业务逻辑层
@Repository 数据层
@Configuration 配置层
@Component 组件层

注意:控制层只能使用@Controller,而不能使用别的注解,但是其他的注解可以相互替换。即使这样,在实际开发中,还是要准守规范,不能随性使用。

方法注解@Bean

类注解是添加到类上,虽然种类丰富,但是存在两个致命的问题:

1、无法为外部的类添加注解(如:第三方库);

2、无法为同一个类创建多个对象;

第一点很好解释,因为外部的类,我们是无法进行修改的。至于第二点,我们之前在根据类型 和 Bean的name拿到的三个对象都是同一个对象,因此是无法实现为同一个类创建多个对象的。但是作为方法注解的@Bean却可以做到。针对第一点的话,只需新建一个Java类,为其加上类注解,然后创建一个方法,加上@Bean注解,最后只需要返回外部类的实例即可。

注意:Spring在扫描的过程中,扫描的基本单位是类,因此@Bean注解常常和类注解一起使用,这样Spring就会扫描到对应的类,接着再去扫描@Bean注解标注的方法。当扫描到@Bean注解标注的方法时,就会去执行一次该方法,最终返回的对象就会加入到Spring容器中。

至于第二点,我们可以用代码来观察:

@Component
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String name;
    private Integer age;
}
@Component
public class UserComponent {
    public User s1() {
        return new User("张三",20);
    }

    public User s2() {
        return new User("李四",30);
    }
}

基于上述代码,我们去获取User对象时,只会获取到一个对象:

上述方法只能获取到同一个对象,而当我们需要多个对象时,完全做不到。我们在观察加上@Bean注解之后。

@Component
public class UserComponent {
    @Bean
    public User s1() {
        return new User("张三",20);
    }

    @Bean
    public User s2() {
        return new User("李四",30);
    }
}

再去运行代码,会发现直接就抛异常了:

这个异常信息是说Bean对象并不是唯一的,也就说Spring在获取Bean对象时(根据类型),这个Bean对象存在多个,它分不清楚,直接把抛异常了。这是因为我们把User类交给了Spring进行管理,这个对象已经存在了,这里的@Bean注解标注的两个方法也都执行了,又实例化了两个USer对象。这个也可以从报错信息的描述看出来:

No qualifying bean of type 'com.springboot.springiocdemo.pojo.User' available: expected single matching bean but found 3: user,s1,s2 
 

这里的user,s1,s2 就是这三个对象的名字,因此我们得通过Bean的name来获取,而不能通过单独的类型来获取(也可以两者组合获取)。

从图中的结果看来,Spring容器中,类型为USer的对象确确实实有三个。

重命名Bean 

Spring为了更好地管理Bean时,为每一个Bean都起了名字,这就类似于我们班级学生的管理,每个学生都有自己的学号。但是有时候,我们认为Spring给对象起的名字并不符合我们的要求,我们想要修改也是可以的。

@Component
public class UCComponent {
    public void print() {
        System.out.println("do UCComponent");
    }
}

上面创建了一个名为UCComponent的类,经过我们上面的学习,已经知道了当类名的前两个字母为大写时,Bean的name就是类名本身,因此下面我们在通过Bean的name去获取Bean的时候,应该使用的是原类名:

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        // 我们可以通过Spring的上下文来获取容器中的对象
        UCComponent bean = (UCComponent)context.getBean("UCComponent");
        bean.print();
    }

}

但我们突然想换一个name,使用类名的全小写:uccomponent。

@Component("uccomponent")
public class UCComponent {
    public void print() {
        System.out.println("do UCComponent");
    }
}

 如果我们不修改获取的Bean name,那么最终代码会抛异常:

修改name之后: 

五大注解都可以使用上述的方式进行修改默认的name,我们这里修改的value属性的值,但由于value可以省略。因此这里可以直接写修改后的name。 

同样其余四个注解:@Controller、@Service、@Repository、@Configuration都是使用上面这种方式来修改默认的name的。

@Bean标注的方法被Spring管理的对象名默认是对应的方法名。我们也可以进行修改。

上面是@Bean的源码,可以看到value属性和name属性是相对应的,且两者都是String类型的数组,也就是说@Bean的name可以是一个数组: 

@Component
public class UserComponent {
    // 当只有一个值时,{} 可以省略
    @Bean("user1")
    public User s1() {
        return new User("张三",20);
    }

    // 下面三种方式都是满足的
    @Bean(value = {"user2", "USer2", "USER2"})
//    @Bean(name = {"user2", "USer2", "USER2"})
//    @Bean({"user2", "USer2", "USER2"})
    public User s2() {
        return new User("李四",30);
    }
}

注意:

1、五大注解的value属性都是String类型,而@Bean的value(name)却是一个String类型的数组。

2、@Bean想要生效,必须确保其类所在的类被Spring扫描到,加上五大注解之后,其类就会被Spring扫描,最常见的是@Bean配合@Configuration使用。

扫描路径

Spring扫描的路径是整个项目的全部吗?肯定不是,这个项目中的jar包就有很多,而jar包下的类更是数不胜数。那扫描的是src/main/java文件就行了呀!这个文件里面的代码都是我们自己写的,不会有很多的。这只是在学习阶段写的代码非常少,如果是在企业中,代码同样是非常多的,不可能一个一个文件去扫描,那样项目的启动速度会非常慢。正确的做法配置一个Spring的扫描路径,处于该路径下的所有Java文件均会被扫描到,而默认的扫描路径是启动类所在的包下。

注意:只要是启动类所在的包下的所有子包以及子类都会被扫描。 

除了使用默认的扫描路径之外,如果我们想要修改扫描路径也是可以做到的。

这里我们使用的是@ComponentScan,注意后面并没有s,别搞错了。

其value属性是String[ ],也就意味着可以设置Spring扫描路径为多个。

注意:

1、当我们手动设置了扫描路径之后,默认的扫描路径不在生效了。

2、手动设置的扫描类下可以没有main方法,但是启动项目至少得有一个main方法。如果我们不想使用默认提供的启动类中的main方法来启动项目,就可以手动创建一个main方法,不过要注意的是:启动SpringBoot项目,得在main方法中,添加下面的代码:

SpringApplication.run(SpringIocDemoApplication.class, args);

 3、扫描该类,并不是代表该类中的main方法会被执行,而是将符合条件的类注册为 Spring 容器中的Bean而已。

DI详解

DI的全称是Dependency Injection,即依赖注入。依赖注入是一个过程,是指IOC容器在创建Bean时,去提供运行时所依赖的资源,而资源指的就是对象。用我们的话说,就是指对象的赋值操作是由IOC容器本身完成的。

关于依赖注入的方式,Spring提供了三种:属性注入、构造方法注入、setter方法注入(容器通过反射机制来实现的)。

属性注入

属性注入的方式通过@Autowired注解来完成的,当我们在某个属性上方加上了@Autowired注解之后,Spring在扫描到该注解时,就会在IOC容器中,根据类型去查找所对应的对象,如果找到的话,就会进行属性注入,给相应的对象进行赋值,反之,如果没找到的话,就会抛异常。

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        HelloController bean = context.getBean(HelloController.class);
        bean.print();

    }

}


@Controller
public class HelloController {

    @Autowired
    private HelloService helloService;

    public void print() {
        System.out.println("do HelloController");
        System.out.println("=======================");
        // 如果helloService有值的话,这里就会打印出值;反之,则会抛异常
        helloService.print();
    }
}


@Service
public class HelloService {
    public void print() {
        System.out.println("do Service");
    }
}

 最终的执行结果:

我们再来看容器中没有改类型的情况:

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        HelloController bean = context.getBean(HelloController.class);
        bean.print();

    }

}


@Controller
public class HelloController {

    @Autowired
    private HelloService helloService;

    @Autowired
    private User user;

    public void print() {
        System.out.println("do HelloController");
        System.out.println("=======================");
        // 如果helloService有值的话,这里就会打印出值;反之,则会抛异常
        helloService.print();
        System.out.println(user);
    }
}



@Service
public class HelloService {
    public void print() {
        System.out.println("do Service");
    }
}



public class User {
    private String name;
    private Integer age;
}


没有找到,项目直接启动不起来。

构造方法注入

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        HelloController bean = context.getBean(HelloController.class);
        bean.print();

    }

}


@Controller
public class HelloController {

    private HelloService helloService;

    public HelloController(HelloService helloService) {
        this.helloService = helloService;
    }

    public void print() {
        System.out.println("do HelloController");
        System.out.println("=======================");
        // 如果helloService有值的话,这里就会打印出值;反之,则会抛异常
        helloService.print();
    }
}



@Service
public class HelloService {
    public void print() {
        System.out.println("do Service");
    }
}

上述就是使用构造方法注入的代码,并且执行成功了。

 有小伙伴可能会好奇,构造方法注入难道不需要@Autowired注解吗?需要,但和属性注入不一样,不是必须需要。我们先来看当有多个构造方法时,Spring默认会使用哪个构造方法进行注入呢?

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        HelloController bean = context.getBean(HelloController.class);
        bean.print();

    }

}


@Controller
public class HelloController {

    private HelloService helloService;
    private User user;

    public HelloController() {
    }

    public HelloController(HelloService helloService) {
        this.helloService = helloService;
    }

    public HelloController(HelloService helloService, User user) {
        this.helloService = helloService;
        this.user = user;
    }

    public void print() {
        System.out.println("do HelloController");
        System.out.println("=======================");
        // 如果helloService有值的话,这里就会打印出值;反之,则会抛异常
        helloService.print();
        System.out.println(user);
    }
}



@Service
public class HelloService {
    public void print() {
        System.out.println("do Service");
    }
}



public class User {
    private String name;
    private Integer age;
}

从运行结果来看,程序抛异常了,说是helloService为空,也就是说在面对过个构造方法,进行依赖注入时,Spring选择的默认的构造方法是无参的构造方法。那如果我们将无参的构造方法去掉,Spring会选择哪一个呢?

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        HelloController bean = context.getBean(HelloController.class);
        bean.print();

    }

}


@Controller
public class HelloController {

    private HelloService helloService;
    private User user;

    public HelloController(HelloService helloService) {
        this.helloService = helloService;
    }

    public HelloController(HelloService helloService, User user) {
        this.helloService = helloService;
        this.user = user;
    }

    public void print() {
        System.out.println("do HelloController");
        System.out.println("=======================");
        // 如果helloService有值的话,这里就会打印出值;反之,则会抛异常
        helloService.print();
        System.out.println(user);
    }
}



@Service
public class HelloService {
    public void print() {
        System.out.println("do Service");
    }
}



public class User {
    private String name;
    private Integer age;
}

我们会发现,当默认的构造方法被去掉时,也会抛异常:告诉我们默认的构造方法没找到。这里就有小伙伴疑惑:最开始只有一个构造方法并且也不是默认的无参构造方法时,为什么依赖注入成功了,而这里有两个就不行了呢?Spring在进行依赖注入时,如果有多个构造方法的话,且存在无参的构造方法时,Spring就会选择无参的构造方法进行依赖注入。如果有多个构造方法的话,且不存在无参的构造方法时,Spring就会在这些构造方法中寻找无参的构造方法,结果是找不到的,因此就会直接抛异常。如果只存在一个构造方法的话,Spring就不会去分辨,而是直接去使用了。除了使用Spring默认的注入方式之外,我们也可以手动指定Spring的注入方式,通过@Autowired注解来指定Spring使用啥样的构造方法进行依赖注入。

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        HelloController bean = context.getBean(HelloController.class);
        bean.print();

    }

}


@Controller
public class HelloController {

    private HelloService helloService;
    private User user;

    public HelloController(HelloService helloService) {
        this.helloService = helloService;
    }

    @Autowired
    public HelloController(HelloService helloService, User user) {
        this.helloService = helloService;
        this.user = user;
    }

    public void print() {
        System.out.println("do HelloController");
        System.out.println("=======================");
        // 如果helloService有值的话,这里就会打印出值;反之,则会抛异常
        helloService.print();
        System.out.println(user);
    }
}



@Service
public class HelloService {
    public void print() {
        System.out.println("do Service");
    }
}


@Component
public class User {
    private String name;
    private Integer age;
}

从程序的运行结果,我们可以看出:确实可以通过@Autowired注解手动指定Spring的注入方式。但要注意的是,我们需要将User类交由Spring管理才行,即加上五大注解。

setter方法注入

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
        HelloController bean = context.getBean(HelloController.class);
        bean.print();

    }

}


@Controller
public class HelloController {
    private HelloService helloService;
    private User user;

    @Autowired
    public void setHelloService(HelloService helloService) {
        this.helloService = helloService;
    }

    @Autowired
    public void setUser(User user) {
        this.user = user;
    }

    public void print() {
        System.out.println("do HelloController");
        System.out.println("=======================");
        // 如果helloService有值的话,这里就会打印出值;反之,则会抛异常
        helloService.print();
        System.out.println(user);
    }
}


@Service
public class HelloService {
    public void print() {
        System.out.println("do Service");
    }
}


@Component
public class User {
    private String name;
    private Integer age;
}

注意:setter方法注入和属性注入是一样的,都是要手动加上@Autowired注解,Spring才会去进行依赖注入。 

依赖注入三种方式的优劣势

属性注入

优点:非常方便,代码量非常少。

缺点:只能用于IOC容器;不能注入final修饰的属性。

构造方法注入(Spring 4推荐写法)

优点:可以注入final修饰的属性;注入的对象不会被修改;依赖的对象在使用前一定会被完全初始化;通用性好。

缺点:需要注入多个对象时,代码写起来非常繁琐。

setter方法注入(Spring 3推荐写法)

优点:可以在类实例化之后,重新对其中的对象进行配置或注入。

缺点:只能用于不能注入final修饰的属性;注入的对象有会被修改的风险。 

注意:

1、虽然属性注入和setter方法注入都是使用@Autowired注解来实现依赖注入的,但是setter方法注解并不是只依赖于@Autowired注解来实现依赖注入,我们还可以手动去调用它,即调用setter方法本身即可,但是属性却拿不到(不通过反射的情况下,即使通过反射进行赋值,为什么不采用容器呢?容器本身就是采用反射的方式实现DI的呀!)。

2、所有的依赖注入都是需要通过Spring上下文来获取Bean对象从而可以观察到的,如果我们是直接去new该对象的话,和SE阶段学习的语法是一样的,并不会有依赖注入的过程。

@Autowired注解的缺陷

经过前面的学习,我们已经知道了@Autowired注解是根据类型在IOC容器中查找对象的,但我们知道如果存在多个对象的话,根据类型查找就做不到了。

@SpringBootApplication
public class SpringIocDemoApplication {

    public static void main(String[] args) {
        // 返回的是Spring的上下文
        ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);

        HelloController bean = context.getBean(HelloController.class);
        bean.print();
    }

}


@Controller
public class HelloController {
    @Autowired
    private User user;

    public void print() {
        System.out.println(user);
    }
}



@Component
public class UserComponent {
    @Bean("user1")
    public User s1() {
        return new User("张三",20);
    }

    @Bean({"user2", "USer2", "USER2"})
    public User s2() {
        return new User("李四",30);
    }
}


@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String name;
    private Integer age;
}

如果存在多个对象,我们要么使用@Primary注解来指定使用哪个注解为主,要么使用@Qualifier注解来指定使用哪个名字的注解为主。 

1、使用@Primary注解(在面对同类型的多个对象时,指定优先级更高):

2、使用@Qualifier注解(在面对同类型的多个对象时,根据名字来使用): 

注意:

1、@Qualifier注解必须配合@Autowired注解使用。因为@Qualifier注解是在面对同类型多对象时,@Autowired注解识别不了时,@Qualifier注解通过名字来辅助识别。

2、如果@Autowired注解找到的对象和@Qualifier注解中的value属性标识的对象名不一样的话,也是会抛异常的:

3、除了上述两种使用Spring的注解之外,还可以使用JDK的注解:@Resource。 ​​​​​

@Resource注解就是直接通过名字来查找对象,相当于是@Autowired + @Qualifier。同样没找到也是直接抛异常。

注意:使用@Resource注解在指定注入的对象名时, 前面的"name="是不能省略的。

如果有小伙伴尝试过的话,会发现在最开始的情景下,User类上加上五大注解的任意一个注解时,都是可以成功注入的:

之所以程序能启动成功,是因为在User类上方加入五大注解类的任意一个时,IOC容器中会出现三个User类对象:user1、user2、user。而在DI部分的代码所写的对象名就是user,因此容器会在这三个对象中去查找是否存在名为user的Bean,由于存在所以刚好可以注入成功。如果我们把User默认的Bean name给修改成非name话或者把DI的name给修改的话,程序就启动不起来了。

1、修改Bean name: 

2、修改DI的name:

所以严格来说,@Autowired在根据类型查找到多个对象之后,再会根据DI的name去多个对象中查找,如果还找不到,就会抛异常;反之,如果找到了的话,就不会抛异常了。

注意:Spring容器不会区分Bean的名称大小写。 

常见面试题:

@Autowird与 @Resource的区别:
1、@Autowired是Spring框架提供的注解,而@Resource是JDK提供的注解
2、@Autowired默认是按照类型注入(这里不考虑DI的name查找),而@Resource是按照名称注入。相比于@Autowired来说,@Resource 支持更多的参数设置,例如 name 设置,根据名称获取 Bean。

好啦!本期 初始JavaEE篇 —— Spring IOC 和 DI 的学习之旅 就到此结束啦!我们下一期再一起学习吧!