目录
1 IoC&DI
1.1 什么是容器
我们知道Spring是包含了众多工具方法的IoC容器,那么什么是容器,什么是IoC容器,在此之前我们学过一些数据结构,其中List/Map—>数据存储容器,Tomcat->Web存储容器。
1.2 什么是IcO
IcO是Spring的核心思想,在类上面添加@RestController和@Controller注解,就是把这个对象交给Spring管理,Spring框架启动时就会自动加载该类,把对象交给Spring管理,这就是IoC思想,IoC:Inversion of Control(控制反转),也就是说Spring是一个"控制反转"的容器。
2 IoC介绍
我们的需求是造一辆车
2.1 传统程序开发
先设计轮子,根据轮子的大小设计底盘,根据底盘设计车身,根据车身设计整个汽车,这时候就形成了一个依赖的关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.run();
}
static class Car {
private Framework framework;
public Car() {
framework = new Framework();
System.out.println("car init...");
}
public void run() {
System.out.println("car run...");
}
}
static class Framework {
private Bottom bottom;
public Framework() {
bottom = new Bottom();
System.out.println("framework init...");
}
}
static class Bottom {
private Tire tire;
public Bottom() {
tire = new Tire();
System.out.println("bottom init...");
}
}
static class Tire {
private int size;
public Tire() {
this.size = 17;
System.out.println("轮子尺寸:" + size);
}
}
}
2.2 问题分析
上面的代码是没有问题的,但是维护性却很低,如果需求变更的话,随着对车的需求量越来越大,个性化需求也会越来越多,这个时候需要对代码进行修改。
当修改轮子的尺寸,整个代码都需要从头到尾修改,完整代码如下
public class Main {
public static void main(String[] args) {
Car car = new Car(20);
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) {
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);
}
}
}
从上面代码的修改中可以看出,当最底层的代码修改之后,整个调用链上的代码都需要修改,这也说明了,程序的耦合是非常高的
2.3 解决方案
在上面的程序中,我们是根据轮子的尺寸设计底盘的,当轮子的尺寸修改后,底盘也需要修改,依次所有的东西都要改,因此我们先设计汽车,然后再根据汽车来设计车身,依次往下,这时候关系就翻转过来了。
实现方案:在每个类中自己创建下级类,如果自己创建下级类会发生改变操作,后面也要跟着修改。因此改为传递的方式(注入的方式),这样我们不需要在当前类创建下级类,所以下级类发生变化,当前类也无需修改任何代码了,这样就完成了程序的解耦。
public class Main {
public static void main(String[] args) {
Tire tire = new Tire(17);
Bottom bottom = new Bottom(tire);
Framework framework = new Framework(bottom);
Car car = new Car(framework);
car.run();
}
}
static public class Car {
private Framework framework;
public Car(Framework framework) {
this.framework = framework;
System.out.println("car init....");
}
public void run() {
System.out.println("car run");
}
}
static public class Framework {
private Bottom bottom;
public Framework(Bottom bottom) {
this.bottom = bottom;
System.out.println("framework init....");
}
}
static public class Bottom {
private Tire tire;
public Bottom(Tire tire) {
this.tire = tire;
System.out.println("bottom init....");
}
}
static public class Tire {
private int size;
public Tire(int size) {
this.size = size;
System.out.println("tire init....size:" + size);
}
}
代码经过上述调整,无论底层图和变化,整个调用链不需要做任何变化,这样就完成了代码之间的解耦,此时实现了更加灵活、通用的程序设计了
2.4 IoC优势
使用IoC设计,就是将控制权发生反转,不再是使用方对象创建并控制依赖对象了,而是把依赖对象注入当前对象中,依赖对象的控制权不再由当前类控制了,因此IoC容器就是控制反转容器。
IoC容器的优点:
1)资源集中管理:IoC容器会帮我们管理一些资源,当我们需要使用时,只需要从IoC容器中取就可以了
2)再创建实例的时候不需要了解其中的细节,降低了使用资源双方的依赖程度,也就是耦合度
3 DI介绍
DI:Dependency Injection(依赖注入)
程序再运行时需要某个资源,此时容器就为其提供这个资源,就是通过引入IoC容器,利用依赖关系注入的方式,实现对象之间的解耦
IoC是一种思想,也就是"目标",而思想只是一种指导原则,最终还需要可行的方案,而DI就是具体的实现,所以说,DI是IoC的一种实现
4 IoC & DI使用
因为Spring是一个IoC容器,作为容器,那么他就具备两个最基础的功能:存和取,Spring容器管理的主要是对象,这些对象我们称之为"Bean",把这些对象交给Spring管理,由Spring来负责对象的创建和销毁,只需要告诉Spring,哪些需要存,怎样从Spring中取对象
通过@Component存对象,@Autowired取对象
5 Bean的存储
Spring框架为了更好的服务web应用程序,提供了更丰富的注解
1)类注解:@Controller @Service @Repository @Component @Configuration
2)方法注解:@Bean
5.1 @Controller(控制器存储)
使用@Controller存储bean的代码如下
@Controller//将对象存储到Spring中
public class UserController {
public void doController() {
System.out.println("do Controller");
}
}
接下来从Spring容器中获取对象
这里需要注意,需要选择的第一个接口,而不是下面的类
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//从Spring上下文获取对象
UserController userController = context.getBean(UserController.class);
//使用对象
userController.doController();
}
}
此时就能成功的从Spring中获取到Controller,并执行Controller的doController方法
如果把@Controller去掉,在观察
这个时候会出现报错,找不到UserController的Bean了
5.2 Bean命名约定
在开发中不需要为bean指定名称,Spring容器将为该bean生成唯一的名称
命名约定使用Java标准约定作为实例字段名,bean名称以小写字母开头,然后使用驼峰式的大小写,如以下
类名:UserController,Bean的名称:userController
类名:AccountManager,Bean的名称为:accountManager
特殊情况,当有多个字符并且第一个和第二个字符都是大写,此时Bean的命名为类名,例如
类名:UController,Bean的名称:UController
Bean的获取有三种方法,获取代码如下
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//根据bean类型,从Spring上下文获取对象
UserController userController1 = context.getBean(UserController.class);
//根据bean名称,从Spring上下文获取对象
UserController userController2 = (UserController) context.getBean("userController");
//根据bean的类型+名称,从Spring上下文获取对象
UserController userController3 = context.getBean("userController", UserController.class);
System.out.println(userController1);
System.out.println(userController2);
System.out.println(userController3);
}
}
我们观察运行结果,输出的地址是一样的,说明对象是一个
常见面试题
ApplicationContent VS BeanFactory
从继承关系和功能来说:Spring容器有两个顶级接口ApplicationContent和BeanFactory,其中BeanFactory提供了基础的访问容器能力,而ApplicationContent属于BeanFactory的子类,它继承了BeanFactory的所有功能
从性能方面来说:ApplicationContent是一次性加载并初始化所有的Bean对象,而BeanFactory更加轻量
5.3 @Service(服务存储)
使用@Service存储bean的代码如下
@Service
public class UserService {
public void doService() {
System.out.println("do Service");
}
}
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//从Spring上下文获取对象
UserService userService = context.getBean(UserService.class);
userService.doService();
}
}
5.4 @Repository(仓库存储)
使用@Repository存储bean的代码如下
@Repository
public class UserRepository {
public void doRepository() {
System.out.println("do Repository");
}
}
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//从Spring上下文获取对象
UserRepository userRepository = context.getBean(UserRepository.class);
userRepository.doRepository();
}
}
5.5 @Component(组件存储)
使用@Component存储bean的代码如下
@Component
public class UserComponent {
public void doComponent() {
System.out.println("do Component");
}
}
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//从Spring上下文获取对象
UserComponent userComponent = context.getBean(UserComponent.class);
userComponent.doComponent();
}
}
5.6 @Configuration(配置存储)
使用@Configuration存储bean的代码如下
@Configuration
public class UserConfiguration {
public void doConfiguration() {
System.out.println("do Configuration");
}
}
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
//获取Spring上下文对象
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
//从Spring上下文获取对象
UserConfiguration userConfiguration = context.getBean(UserConfiguration.class);
userConfiguration.doConfiguration();
}
}
5.7 为什么需要这么多类注解
每个注解都有自己的用途,注解多是让程序员看到类注解后,能够直接了解当前类的用途
@Controller:控制层,接收请求,返回响应
@Service:业务逻辑层,处理具体的业务逻辑
@Repository:数据访问层,也称为持久层,负责数据的访问操作
@Configuration:配置层,处理项目中的一些配置信息
程序的应用分层,调用流程如下:
类注解之间的关系
查看@Controller @Service @Repository @Configuration 等注解的源码发现:
这些注解里面都有一个注解@Component,说明它们本身就是@Component的子类,@Component是一个元注解,可以注解其他类注解,如@Controller @Service @Repository @Configuration,它们也是@Component的衍生注解。在开发中,如果想在控制层使用@Component也是可以的,但是一般建议在控制层使用@Controller
5.8 方法注解@Bean
类注解(五大注解)是添加到某个类上的,但是存在两个问题:
1)第三方jar包是没有办法添加五大注解
2)一个类,定义多个对象时,比如多个数据源
这种场景的话就需要使用@Bean注解
public class BeanConfig {
@Bean
public UserInfo userInfo() {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setUserName("zhangsan");
userInfo.setAge(10);
return userInfo;
}
}
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserInfo userInfo = context.getBean(UserInfo.class);
System.out.println(userInfo);
}
}
此时我们没法获取bean对象中的user,这是因为在Spring框架设计中,方法注解@Bean需要配合 类注解(五大注解)才能将对象正常存储到Spring容器中
@Configuration
public class BeanConfig {
@Bean
public UserInfo userInfo() {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setUserName("zhangsan");
userInfo.setAge(10);
return userInfo;
}
}
5.9 定义多个对象
在同一个类中定义多个对象,根据类型获取对象,获取的是哪个对象?
@Configuration
public class BeanConfig {
@Bean
public UserInfo userInfo() {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setUserName("zhangsan");
userInfo.setAge(10);
return userInfo;
}
@Bean
public UserInfo userInfo2() {
UserInfo userInfo = new UserInfo();
userInfo.setId(2);
userInfo.setUserName("lisi");
userInfo.setAge(11);
return userInfo;
}
}
此时报错信息显示:期望有一个匹配,结果发现了两个,userInfo,userInfo2
从报错信息中可以看出,@Bean注解的bean,bean的名称就是它的方法名,下面根据名称来获取Bean对象
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserInfo userInfo = (UserInfo) context.getBean("userInfo");
System.out.println(userInfo);
UserInfo userInfo2 = (UserInfo) context.getBean("userInfo2");
System.out.println(userInfo2);
}
}
可以看到,@Bean可以针对同一个类定义多个对象,当同一个类有多个对象时,通过bean的名称,也就是方法名来获取对象
5.10 @Bean传递参数
@Configuration
public class BeanConfig {
@Bean
public UserInfo userInfo(String name) {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setUserName(name);
userInfo.setAge(10);
return userInfo;
}
}
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserInfo userInfo = context.getBean(UserInfo.class);
System.out.println(userInfo);
}
}
控制台会出现"需要一个String类型的对象"
@Configuration
public class BeanConfig {
@Bean
public String name() {
return "zhangsan";
}
@Bean
public UserInfo userInfo(String name) {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setUserName(name);
userInfo.setAge(10);
return userInfo;
}
}
5.11 重命名Bean
可以通过设置name属性给Bean对象进行重命名操作
@Configuration
public class BeanConfig {
@Bean("user1")
public UserInfo userInfo() {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setUserName("zhangsan");
userInfo.setAge(10);
return userInfo;
}
}
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserInfo user1 = (UserInfo) context.getBean("user1");
System.out.println(user1);
}
}
5.12 扫描路径
使用五大注解声明的bean想要生效,关键的一点时需要被Spring扫描
将DemoApplication放到configuration这个包里面,再次观察结果,发现没有找到bean对象
报错的原因就是使用五大注解声明的bean,要想生效,还需要配置扫描路径,让Spring扫描到这些类,通过 @ComponentScan来配置扫描路径
@ComponentScan({"com.example.demo"})
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserInfo user1 = (UserInfo) context.getBean("user1");
System.out.println(user1);
}
}
前面没有配置@ComponentScan注解也能扫描到,这是因为SpringBoot的特点是,约定大于配置,默认的扫描路径是启动类所在的目录以及子目录
最开始启动类所在的位置就是com.example.demo的路径下,因此它能扫描到这个目录下的子类
6 DI详情
上面说了控制反转IoC的细节,现在是依赖注入DI的细节
依赖注入是一个过程,是指IoC容器在创建Bean时,取提供运行所依赖的资源,资源就是对象,关于依赖的注入,Spring提供了三种方式
1)属性注入
2)构造方法注入
3)Setter注入
6.1 属性注入
使用@Autowired实现,将Service类注入到Controller类中
Service类的实现代码
@Service
public class UserService {
public void doService() {
System.out.println("do Service");
}
}
Controller类的实现代码
@Controller
public class UserController {
@Autowired
private UserService userService;
public void doController() {
userService.doService();
System.out.println("do Controller");
}
}
获取Controller中的doController方法
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserController userController = context.getBean(UserController.class);
userController.doController();
}
}
6.2 构造方法注入
@Controller
public class UserController {
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
public void doController() {
userService.doService();
System.out.println("do Controller");
}
}
注意:如果类里面只有一个构造方法,那么 @Autowired注解可以省略,如果有多个构造方法,此时就需要添加@Autowired来明确指定到底使用哪个构造方法,有多个构造方法的时候,如果没有@Autowired注解,默认会调用无参的构造方法,此时会报空指针异常
6.3 Setter注入
Setter注入和属性的Setter方法实现类似,只不过在设置set方法的时候加上@Autowired注解
@Controller
public class UserController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void doController() {
userService.doService();
System.out.println("do Controller");
}
}
去掉@Autowired注解的话会报空指针异常
6.4 三种注入优缺点
属性注入:
优点:简洁,使用方便
缺点:只能用于IoC容器,如果是非IoC容器不可用,并且只有在使用的时候才会出现空指针异常
不能注入一个Final修饰的属性
构造方法注入:(Spring4.X推荐)
优点:可以注入Final修饰的属性
注入的对象不会被修改
依赖对象在使用前一定会被初始化,因为依赖在类的构造方法中执行,而构造方法是在类 加载阶段就会执行的方法
通用性好,构造方法时JDK支持的,更换任何框架,都是适用的
缺点:注入多个对象时,代码比较繁琐
Setter注入:(Spring3.X推荐)
优点:方法在实例之后,重新对该对象进行配置或者注入
缺点:不能注入Final修饰的属性
注入的对象可能会改变,因为Setter方法可能被多次调用,就有修改的风险
6.5 @Autowired存在问题
当同一类型存在多个bean,也就是多个对象时,使用@Autowired会存在问题
@Configuration
public class BeanConfig {
@Bean("user1")
public UserInfo userInfo() {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setUserName("zhangsan");
userInfo.setAge(10);
return userInfo;
}
@Bean
public UserInfo userInfo2() {
UserInfo userInfo = new UserInfo();
userInfo.setId(2);
userInfo.setUserName("lisi");
userInfo.setAge(11);
return userInfo;
}
}
@Controller
public class UserController {
@Autowired
//注入userInfo
private UserInfo userInfo;
public void doController() {
System.out.println("do Controller");
System.out.println(userInfo);
}
}
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserController userController = context.getBean(UserController.class);
userController.doController();
}
}
报错的原因是,非唯一的Bean对象
如何解决上述问题,Spring提供了以下几种解决方案:
1)@Primary 2)@Qualifier 3)@Resource
6.5.1 使用@Primary注解
当存在多个相同类型的Bean是注入时,加上@Primary注解,来确定默认的实现
@Configuration
public class BeanConfig {
@Primary
@Bean("user1")
public UserInfo userInfo() {
UserInfo userInfo = new UserInfo();
userInfo.setId(1);
userInfo.setUserName("zhangsan");
userInfo.setAge(10);
return userInfo;
}
@Bean
public UserInfo userInfo2() {
UserInfo userInfo = new UserInfo();
userInfo.setId(2);
userInfo.setUserName("lisi");
userInfo.setAge(11);
return userInfo;
}
}
@Controller
public class UserController {
@Autowired
//注入userInfo
private UserInfo userInfo;
public void doController() {
System.out.println("do Controller");
System.out.println(userInfo);
}
}
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
UserController userController = context.getBean(UserController.class);
userController.doController();
}
}
6.5.2 使用@Qualifier注解
使⽤@Qualifier注解:指定当前要注⼊的bean对象。在@Qualifier的value属性中,指定注⼊的bean的名称,@Qualifier注解不能单独使用,必须配合@Autowired使用
@Controller
public class UserController {
@Qualifier("userInfo2")
@Autowired
//注入userInfo
private UserInfo userInfo;
public void doController() {
System.out.println("do Controller");
System.out.println(userInfo);
}
}
6.5.3 使用@Resource注解
使⽤@Resource注解:是按照bean的名称进⾏注⼊,通过name属性指定要注⼊的bean的名称
@Controller
public class UserController {
@Resource(name = "user1")
//注入userInfo
private UserInfo userInfo;
public void doController() {
System.out.println("do Controller");
System.out.println(userInfo);
}
}
常见面试题:
@Autowird 与 @Resource的区别
1 @Autowired是Spring框架提供的注解,而@Resource是JDK提供的注解
2 @Autowired默认是按照类型注入的,如果同一个类型存在多个对象,按名称匹配,如果名称匹配补上,就会报错,而@Resource是按照名称注入,相比@Autowired来说,@Resource支持更多的参数,例如name属性,根据名称获取Bean