找往期文章包括但不限于本期文章中不懂的知识点:
个人主页:我要学编程程(ಥ_ಥ)-CSDN博客
所属专栏:JavaEE
目录
应用分层
初始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 的学习之旅 就到此结束啦!我们下一期再一起学习吧!