关注我,学习更多企业开发和面试内容~
1. IoC & DI的概念
Spring是一个包含众多工具的IoC容器,容器能装东西。比如List/Map是数据存储容器,Tomcat是Web容器。
- IoC:控制反转。
- Spring也是一个容器,装的是对象。控制反转,又称为控制权反转,也就是获取依赖对象的过程被反转了。之前需要通过自己new创建对象,现在是直接把创建对象的任务交给容器,程序中只需要依赖注入(DI)就可以了。这个容器称为IoC容器,所以有时Spring也称为Spring容器。
- 控制反装是一种思想,生活中比如,自动驾驶(比如驾驶员不需要驾驶,让自动化系统来控制)。
- IoC容器:对类添加特定的注解,就是把这个对象交给Spring管理,Spring框架启动时会加载该类,把对象交给Spring管理,就是IoC思想。
【面试题】Spring两大核心思想:IoC和AOP。
2. IoC
以造车为例。
2.1 传统程序开发
轮胎(Tire)依赖底盘 (Bottom)依赖车身(Framework)依赖汽车(Car)。我们可以尝试不在每个类中自己创建new下级对象,因为如果自己创建下级类就会出现当下级类发生改变操作,自己也要跟着修改。
只是给car类的构造方法加了一个参数,所有调用者都需要修改,这就是耦合太高了。
public class Tire {
private Integer size;
public Tire(Integer size) {
this.size = size;
System.out.println("Tire init...");
}
}
public class Bottom {
private Tire tire;
//修改代码,添加size参数 //调用方第1次修改代码
public Bottom(int size) {
this.tire = new Tire(size);
System.out.println("framework init...");
}
}
public class Framework {
private Bottom bottom;
// public Framework() {
// this.bottom = new Bottom();
// System.out.println("framework init...");
// }
public Framework(int size) {
this.bottom = new Bottom(size); //调用方第1次修改代码
System.out.println("framework init...");
}
}
public class Car {
private Framework framwork;
// public Car() {
// this.framwork = new Framework();
// System.out.println("car init...");
// }
public Car(int size) {
this.framwork = new Framework(size); //调用方第1次修改代码
System.out.println("car init...");
}
public void run() {
System.out.println("car run...");
}
}
public class Main {
public static void main(String[] args) {
// Car car = new Car();
Car car = new Car(4);
car.run();
}
}
综上所述,增加了1个参数,却需要修改3次代码。
2.2 解耦合形式开发(IoC)
此时,我们只需要将原来由自己创建下级类的对象,改为传递的方式(也就是注入的方式),因为我们不需要在当前类中创建下级类了,所以下级类即使发生变化(创建或减少参数),当前类本身也无需修改任何代码,这样就完成了程序的解耦。
public class Tire {
private Integer size;
private String color;
public Tire(Integer size,String color) {
this.size = size;
this.color = color;
System.out.println("Tire init...");
}
}
public class Bottom {
private Tire tire;
public Bottom(Tire tire) {
this.tire = tire;
}
}
public class Framework {
private Bottom bottom;
public Framework(Bottom bottom) {
this.bottom = bottom;
}
}
public class Car {
Framework framework;
public Car(Framework framework) {
this.framework = framework;
}
public void run() {
System.out.println("car run...");
}
}
public class Main {
public static void main(String[] args) {
//Spring做的事情如下,创建对象
Tire tire = new Tire(4,"red");
Bottom bottom = new Bottom(tire);
Framework framework = new Framework(bottom);
Car car = new Car(framework);
}
}
在传统的代码中对象创建顺序是:Car->Framework->Bottom->Tire。
改进之后解耦的代码的对象创建顺序是:Tire->Bottom->Framework->Car。
2.3 DI形式开发(IoC)
IoC是思想,DI是实现方式。实现的IoC的方式很多,上面传统的解耦合代码是一种,DI也是一种解耦合。通过依赖注入(DI),将依赖关系的控制权交给外部容器(如 Spring),实现解耦。
依赖:当前类需要的东西,比如jar包。
注入:将依赖作为当前类的属性。
将依赖注入当前类,使当前类和依赖注入的对象打配合,实现IoC,如下图。
- @Autowired:告诉Spring,从容器中取出这个对象,赋值给当前对象的属性。
* Spring IoC和DI
Spring是一个IoC容器,能存和取对象。
Spring容器管理的主要是对象,这些对象,我们称之为"Bean"。我们把这些对象交由Spring管理,由Spring来负责对象的创建和销毁。我们程序只需要告诉Spring,哪些需要存,以及如何从Spring中取出对象。
IoC容器已经存在,它有IoC思想。所以只要学如何把对象交给Spring容器管理(如@Service等),如何把对象从容器取出来(如@Autowired等等),也就是DI。
3. Bean的存储
- 类注解: @Controller、@Service、@Repository、@Component、@Configuration。
- 方法注解: @Bean。
3.1 @Controller(存储控制器)
想被外界访问到,必须用@Controller,不能用其他注解替代,原因是实现原理不一样,注解声明实现看似一样,实则Spring源码完全不一样。
@Controller
public class UserController {
}
先不做验证,只在Service层做验证。
如果不写注解,会报NoSuchBeanDefinitionException,如下。
//不加@Controller
public class BookController {
@Autowired
private BookService bookService;
}
3.2 @Service(存储服务)
@Service
public class UserService {
public void doService(){
System.out.println("do Service...");
}
}
将类存后,取出Bean来验证,因为还没讲DI,先不用@Autowired,而是用其他方式获取。
按类型获取(推荐):
- context.getBean(UserService.class)
- 明确知道 Bean的类型时使用。
- 类型必须唯一,否则抛NoUniqueBeanDefinitionException。
按名称获取(需转型):
- (UserService) context.getBean(“userService”)
- 需要动态获取或处理同名 Bean 时使用,需手动转型。
- 名称错误抛 NoSuchBeanDefinitionException。
名称+类型获取(推荐):
- context.getBean(“userService”, UserService.class)
- 自动类型匹配。
- 需要确保名称和类型双重匹配时使用。
名称默认为小驼峰形式,特殊情况:若类名前两位都为大写,bean的名称为类名(如EXam)。
3.3 @Repository(存储仓库)
@Repository
public class UserRepository {
public void doRepository(){
System.out.println("do Repository...");
}
}
3.4 @Component(存储组件)
@Component
public class UserComponent {
public void doComponent(){
System.out.println("do Component...");
}
}
3.5 @Configuration(存储配置)
@Configuration
public class UserConfig {
public void doConfig(){
System.out.println("do Config...");
}
}
3.6 关于这些类注解
【为什么要这么多类注解】
这个也是和咱们前面讲的应用分层是呼应的。让程序员看到类注解之后,就能直接了解当前类的用途。
- @Controller:控制层,接收请求,对请求进行处理,并进行响应。
- @Service:业务逻辑层,处理具体的业务逻辑。
- @Repository:数据访问层,也称为持久层。负责数据访问操作。
- @Configuration:配置层。处理项目中的一些配置信息。
- @Component:不属于标准分层(Controller/Service/Repository)的辅助类,所有类注解最终都是 @Component 的“特化版本”。
【类注解之间的关系】
源码里这些只是注解的声明,注解真正功能是在Spring源码里实现的,不在注解声明里。
五大注解只能加在类上,并且只能加在自己的代码上,不能加在。
五大注解取多少次,都是同一个地址。也就是说都是同一个对象。
3.7 方法注解@Bean
【使用@Bean的场景】
- 五大注解只能加在类上,并且只能加在自己的代码上,如果我引入了一个第三方Jar包,也希望交给Spring管理,是没有办法加五大注解的。
- 五大注解只能定义一个对象,而@Bean可以对同一个类定义多个对象。对于一个类,需要定义多个对象时,比如数据库操作,定义多个数据源。
@Bean是方法注解,必须搭配五大注解(类注解)来使用。原因:Spring最开始扫描注解时,只扫描类,不扫描方法,若类上有注解,才会扫描类内的方法。
@Configuration
public class BeanConfig {
@Bean
//存在Spring容器里的对象名默认是 name
public String name(){
return "塞尔达";
}
@Bean
//存在Spring容器里的对象名默认是 name333
public String name333(){
return "达";
}
}
【多个@Bean】
- 当一个类型存在多个Bean时,就不能用类型获取对象了,会报NoUniqueBeanDefinitionException。对象名默认是@Bean修饰的方法名,这时可以用名称获取,或名称加类型,唯独不能用类型获取对象。
- 因为两个对象是同类型,Spring不知道取哪个使用名称,不会出错。
根据方法加类型获取,不需要强转。
根据类型获取,不需要强转。
根据方法获取,不需要强转。
3.8 重命名Bean
可以通过设置name属性给Bean对象进行重命名操作。
//重命名的对象名可以有多个
@Bean(name = {"u1","user1"})
public User user1(){
User user = new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
//简化版本
@Bean({"u1","user1"})
public User user2(){
User user = new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
//只有一个名称时,{}可以省略
@Bean("u1")
public User user3(){
User user = new User();
user.setName("zhangsan");
user.setAge(18);
return user;
}
3.9 更改扫描路径@ComponentScan
启动类(Application)的扫描范围:默认路径是启动类所在的目录极其子目录。
将启动类放入子目录,再次启动,会报错。
src/main/java
└── com
└── example
└── myapp ← 这是项目的根包(root package)
├── Application.java ← 启动类应该放在这
├── controller
│ └── UserController.java
├── service
│ └── UserService.java
└── repository
└── UserRepository.java
src/main/java
└── com
└── example
└── myapp
├── config
│ └── Application.java ← 错误:启动类在子包
├── controller
├── service
└── repository
【扫描范围对比】
当启动类在
com.example.myapp
时:
- 自动扫描:com.example.myapp 及其所有子包(controller/service/repository)
当启动类在
com.example.myapp.config
时:
- 自动扫描:com.example.myapp.config 及其子包
- 不会扫描:com.example.myapp.controller 等兄弟包
用注解指定路径:@ComponentScan(“com.example”)
属于Spring的注解才会被Spring管理,@Data不是Spring的注解。
先扫描@ComponentScan(“com.example”)确定路径。
4. DI
4.1 属性注入
不写@Autowired会报异常。
@Controller
public class UserController {
@Autowired
private UserService userService;
public void doController(){
userService.doService();
System.out.println("do Controller...");
}
}
4.2 构造方法注入
只有一个,可以省略掉@Autowired。如果存在多个构造方法,必须用@Autowired指定。
@Controller
public class UserController2 {
//构造方法注入
private final UserService userService;
@Autowired // Spring 4.3+ 可省略@Autowired(当只有一个构造方法时)
public UserController2(UserService userService) {
this.userService = userService;
}
public void sayHi() {
System.out.println("hi, UserController2...");
userService.sayHi();
}
}
有无参构造方法时Spring会优先用无参。
4.3 Setter注入
就算只有一个set方法也不能省略@Autowired。
【@Autowired 的作用】
明确告诉 Spring 此方法需要依赖注入。若省略,Spring会认为这是一个普通方法,不会自动调用它来注入依赖。【Setter 注入的本质】
需要 Spring 主动调用 Setter 方法并传入依赖对象,而 @Autowired就是触发这一行为的标记。
@Controller
public class UserController3 {
// 注入方式3:Setter方法注入
private UserService userService;
@Autowired // 必须保留注解(即使只有一个Setter方法)
public void setUserService(UserService userService) {
this.userService = userService;
}
public void sayHi() {
System.out.println("hi, UserController3...");
userService.sayHi(); // 调用Service方法
}
}
4.4 @Autowired存在的问题
为什么官方不推荐用属性注入?
属性注入以类型进行匹配,与注入的属性名称无关。
无法注入final修饰的属性,定义时就需要赋值或者构造方法中赋值。
如何解决:
- @Primary
- @Qualifier
- @Resource
4.5 三种方式的区别
static类型的变量与类无关,就算没有类他也存在,只是指定了一个路径而已。