一、IoC與DI
名詞解釋:
- spring是一個裝了眾多工具對象的IoC容器。
- IoC思想:對象交給Spring管理,就是IoC思想。
- IoC:Inversion of Control,控制反轉。
控制權反轉,需要某個對象時, 傳統開發模式中需要⾃⼰通過 new 創建對象, 現在不需要再進⾏創建, 把創建對象的任務交給容器(IoC容器. Spring是⼀個IoC容器, 所以有時Spring 也稱為Spring容器), 程序中只需要依賴注⼊ (Dependency Injection, DI)就可以了.
1.1 IoC
實現下面的需求:
在傳統的實現中,我們將每個模塊當成一個類:
public class NewCarExample {
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() {
this.tire = new Tire();
System.out.println("Bottom init...");
}
}
/**
* 輪胎類
*/
static class Tire {
// 尺⼨
private int size;
public Tire(){
this.size = 17;
System.out.println("輪胎尺⼨:" + size);
}
}
}
但是如上面的代碼,如果我們要修改一個參數,會導致整個調用鏈都跟着修改。
我們為解決上面耦合度過高,可以採取: 把由⾃⼰創建的下級類,改為傳遞的⽅式(也就是注⼊的⽅式), 每次調整隻需要調整對應那個類的代碼即可。 這樣⽆論底層類如何變化,整個調⽤鏈是不⽤做任何改變的。
public class IocCarExample {
public static void main(String[] args) {
Tire tire = new Tire(20);
Bottom bottom = new Bottom(tire);
Framework framework = new Framework(bottom);
Car car = new Car(framework);
car.run();
}
static 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 class Framework {
private Bottom bottom;
public Framework(Bottom bottom) {
this.bottom = bottom;
System.out.println("Framework init...");
}
}
static class Bottom {
private Tire tire;
public Bottom(Tire tire) {
this.tire = tire;
System.out.println("Bottom init...");
}
}
static class Tire {
private int size;
public Tire(int size) {
this.size = size;
System.out.println("輪胎尺⼨:" + size);
}
}
}
1.2 DI
DI: Dependency Injection(依賴注⼊) 容器在運⾏期間, 動態的為應⽤程序提供運⾏時所依賴的資源,稱之為依賴注⼊。
就像上面調用關係中:
二、IoC與DI的使用
Spring 是⼀個 IoC(控制反轉)容器,作為容器, 那麼它就具備兩個最基礎的功能: • 存 • 取 Spring 容器 管理的主要是對象, 這些對象, 我們稱之為"Bean". 我們把這些對象交由Spring管理, 由 Spring來負責對象的創建和銷燬. 我們程序只需要告訴Spring, 哪些需要存, 以及如何從Spring中取出對象
我們實現這樣的功能,主要靠兩個註解:
- Service層及Dao層的實現類,交給Spring管理: 使⽤註解: @Component
- 在Controller層 和Service層 注⼊運⾏時依賴的對象: 使⽤註解 @Autowired
像把前面的圖書管理系統的BookController重構。 BookController類:
package com.example.project.controller;
import com.example.project.model.BookInfo;
import com.example.project.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RequestMapping("/book")
@RestController
@Component
public class BookController {
@Autowired
private BookService bookService;
@RequestMapping("/getList")
public List<BookInfo> getList() {
return bookService.getList();
}
}
BookService類:
package com.example.project.service;
import com.example.project.dao.BookDao;
import com.example.project.model.BookInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class BookService {
@Autowired
BookDao bookDao ;
public List<BookInfo> getList() {
List<BookInfo> books = new ArrayList<>();
books = bookDao.mockData();
for (BookInfo book:
books) {
if(book.getStatus() == 1) {
book.setStatusCN("可借閲");
} else {
book.setStatusCN("不可借閲");
}
}
return books;
}
}
BookDao類:
package com.example.project.dao;
import com.example.project.model.BookInfo;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@Component
public class BookDao {
public List<BookInfo> mockData() {
List<BookInfo> books = new ArrayList<>();
for (int i = 0; i < 5; i++) {
BookInfo book = new BookInfo();
book.setId(i);
book.setBookName("書籍" + i);
book.setAuthor("作者" + i);
book.setCount(i * 5 + 3);
book.setPrice(new BigDecimal(new Random().nextInt(100)));
book.setPublish("出版社" + i);
book.setStatus(1);
books.add(book);
}
return books;
}
}
可以看到在類的調用之間,我們是使用的註解,將類作為另一個類的成員。不用自己去new實例。
三、IoC詳解
3.1 Bean的存儲
Bean在上面我們也説了,就是Spring管理起來的對象。
實現將對象交給Spring管理, 共有兩類註解類型可以:
- 類註解:@Controller、@Service、@Repository、@Component、@Configuration.
- ⽅法註解:@Bean.
3.2 @Controller(控制器存儲)
先使用@Controller將類存儲:
package com.example.springioc.controller;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
public void hello() {
System.out.println("Hello");
}
}
從Spring容器中獲取對象:
- 先獲取Spring上下⽂對象
- 從Spring上下⽂中獲取對象
package com.example.springioc.controller;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
//先獲取Spring上下⽂對象
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class,args);
//從Spring上下⽂中獲取對象
UserController userController = context.getBean(UserController.class);
userController.hello();
}
}
3.3 獲取Bean對象
獲取Bean對象主要是ApplicationContext 類下的getBean方法,有下圖中重載。
使用五大類註解讓Spring管理Bean對象的默認取名方式如下官方文檔:
- 將類名轉換為小駝峯形式。
UserController -》 userController - 當前面是兩個即多個大寫字母連在一起,Bean對象名就是類名。
USController -》 USController
Bean對象名也可以使用註解指定名稱,在使用五大註解加上括號即可。栗子: @Controller("name") 使用如下:
package com.example.springioc.controller;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class,args);
UserController bean1 = context.getBean(UserController.class);
bean1.hello();
UserController bean2 = (UserController) context.getBean("userController");
bean2.hello();
UserController bean3 = context.getBean("userController", UserController.class);
bean3.hello();
}
}
3.4 @Service(服務存儲)
使用就加上@Service註解,拿到Bean對象方法不變。
package com.example.springioc.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
void print() {
System.out.println("do Service");
}
}
3.5 @Repository(倉庫存儲)
使用就加上@Repository 註解,拿到Bean對象方法不變。
package com.example.springioc.service;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
void print() {
System.out.println("do Repository");
}
}
3.6 @Component(組件存儲)
使用就加上@Component 註解,拿到Bean對象方法不變。
package com.example.springioc.service;
import org.springframework.stereotype.Component;
@Component
public class UserComponent {
void print() {
System.out.println("do Component");
}
}
3.7 @Configuration(配置存儲)
使用就加上@Configuration註解,拿到Bean對象方法不變。
package com.example.springioc.service;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfiguration {
void print() {
System.out.println("do Configuration");
}
}
3.8 五大註解區別
@Controller @Service @Repository @Configuration這四個註解都是@Component註解的衍生註解。
分這麼多註解就是為了更好地分層(邊界在使用中也沒非常清晰):
- @Controller代表控制層。接收參數返回響應,控制層一定要使用@Controller
- @Service代表服務層
- @Repository代表數據層
- @Configuration代表配置層
- @Component代表組件層
3.9 ⽅法註解@Bean
使用:
package com.example.springioc.controller;
import com.example.springioc.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Bean
public User user() {
return new User("zhangsan",11);
}
public void hello() {
System.out.println("Hello");
}
}
package com.example.springioc.controller;
import com.example.springioc.model.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class,args);
User bean1 = (User) context.getBean("user");
System.out.println(bean1.getName());
}
}
注意事項:
- 使用@Bean註解默認方法名就是管理的Bean對象名。
- @Bean對象重命名可以直接加括號
@Bean("name1"),還可以使用name屬性@Bean(name = "name1"),還可以使用value屬性@Bean(value = "name1"),並且可以傳String數組。 - @Bean註解必須搭配五大類註解使用。
- 當方法有參數的時候,Spring會從容器中根據參數類型去找,是否有這個類型的對象,如果沒有,或者有多個不唯一都會報錯,有唯一一個就會拿這個對象賦值。
四、Spring掃描路徑
Spring默認的掃描路徑是啓動類所在路徑及其子路徑。
當我們要掃描其它路徑的時候,可以使用註解@ComponentScan("需要掃描路徑"),可以傳數組。
其實不怎麼用這個註解,直接啓動類放在所有需要掃描的路徑的最上層包下即可。
五、DI詳解
依賴注⼊是⼀個過程,是指IoC容器在創建Bean時, 去提供運⾏時所依賴的資源,⽽資源指的就是對象。
依賴注⼊, Spring給我們提供了三種⽅式:
- 屬性注⼊(Field Injection)
- 構造⽅法注⼊(Constructor Injection)
- Setter 注⼊(Setter Injection)
5.1屬性注入@Autowired
屬性注⼊是使⽤ @Autowired 註解實現的 注意事項:
- 注入的對象必須是容器中已經有的,也就是使用五大類註解交給Spring管理的。
- @Autowired不能修飾final修飾的成員。
使用:
package com.example.springioc.controller;
import com.example.springioc.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Autowired
private UserService us;
public void hello() {
System.out.println("Hello");
us.print();
}
}
package com.example.springioc;
import com.example.springioc.controller.UserController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class SpringIocDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class,args);
UserController bean = context.getBean(UserController.class);
bean.hello();
}
}
打印結果為 Hello do Service、
5.2 構造方法注入
直接使用構造函數,將上面代碼改成如下也可以使用。
package com.example.springioc.controller;
import com.example.springioc.service.UserService;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService us;
public UserController(UserService us) {
this.userService = us;
}
public void hello() {
System.out.println("Hello");
us.print();
}
}
注意事項:
- 當只有一個構造函數的時候,直接可以注入。
- 當有兩個及以上構造函數的時候,Spring無法辨別使用哪一個構造函數注入,需要在使用的構造函數前加上@Autowired註解。
- 只能在一個構造方法上加上@Autowired註解。
5.3 setter方法注入
直接加上set方法,加上@Autowired註解,將上面代碼改成如下也可以使用。
package com.example.springioc.controller;
import com.example.springioc.service.UserService;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService us;
@Autowired
public void setUserService(UserService us) {
this.us = us;
}
public void hello() {
System.out.println("Hello");
us.print();
}
}
注意事項:
- set方法必須加上@Autowired註解,可以給多個set方法使用註解。
- 不能修飾final修飾的成員的set方法。
優缺點比較:
- 屬性注⼊
- 優點:簡潔,使⽤⽅便;
- 缺點:
- 只能⽤於 IoC 容器,如果是⾮ IoC 容器不可⽤,並且只有在使⽤的時候才會出現NPE(空指針異常)
- 不能注⼊⼀個Final修飾的屬性
- 構造函數注⼊(Spring 4.X推薦)
- 優點:
- 可以注⼊final修飾的屬性
- 注⼊的對象不會被修改
- 依賴對象在使⽤前⼀定會被完全初始化,因為依賴是在類的構造⽅法中執⾏的,⽽構造⽅法是在類加載階段就會執⾏的⽅法.
- 通⽤性好,構造⽅法是JDK⽀持的, 所以更換任何框架,他都是適⽤的
- 缺點:
- 注⼊多個對象時, 代碼會⽐較繁瑣
- Setter注⼊(Spring 3.X推薦)
- 優點:⽅便在類實例之後, 重新對該對象進⾏配置或者注⼊
- 缺點:
- 不能注⼊⼀個Final修飾的屬性
- 注⼊對象可能會被改變, 因為setter⽅法可能會被多次調⽤,就有被修改的⻛險
5.4 @Autowired註解問題及解決
當一個類交給Spring多個對象後,使用@Autowired註解,會無法分辨。
package com.example.springioc.service;
import com.example.springioc.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Bean
public User u1(String name) {
return new User(name,11);
}
@Bean
public User u2() {
return new User("lisi",18);
}
@Bean
public String name () {
return "zhangsan";
}
public void print() {
System.out.println("do Service");
}
}
package com.example.springioc.controller;
import com.example.springioc.model.User;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Resource(name = "u1")
private User user;
public void hello() {
System.out.println("Hello");
System.out.println(user.toString());
}
}
報錯信息:
解決方法: 提供了以下⼏種註解解決:
- @Primary
- @Qualifier
- @Resource
- 使⽤@Primary註解:當存在多個相同類型的Bean注⼊時,加上@Primary註解,來確定默認的實現。例如上面代碼:
@Bean
@Primary
public String name () {
return "zhangsan";
}
- 使⽤@Qualifier註解:指定當前要注⼊的bean對象。在@Qualifier的value屬性中,指定注⼊的bean的名稱,必須與@Autowired一起用。例如上面代碼:
@Autowired
@Qualifier("u1")
private User user;
- 使⽤@Resource註解:是按照bean的名稱進⾏注⼊。通過name屬性指定要注⼊的bean的名稱。@Resource是JDK提供的註解。 例如上面代碼:
@Resource(name = "u2")
private User user;
@Autowired工作流程