三層架構與分層解耦

📢 本文由 gemini-2.5-flash 翻譯

範例引入

取得員工資料,回傳統一回應結果,並在頁面呈現顯示

首先需要匯入 dom4j 依賴,用於解析 XML 檔案

1
2
3
4
5
<dependency>
    <groupId>org.dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>2.1.3</version>
</dependency>

匯入解析 XML 的工具類別 XMLParserUtils、對應的實體類別 Emp、XML 檔案 emp.xml

匯入靜態網頁檔案,放在 resources/static

SpringBoot 專案的靜態資源 (H5 + CSS + JS 等前端資源) 預設存放目錄為 clsspath:/static, classpath:/public, clsspath:/resources

對應 Maven 來說 classpath 為 src/main/resources

編寫 Controller 程式,處理請求,回應資料 (此範例程式碼省略,使用下列三層架構)

三層架構

上例 Emp 程式將資料存取、處理邏輯和接收回應請求放在一個 Controller 裡,使得復用性差、難以維護,為此需要將其分開以滿足單一職責原則,三層架構使得程式碼復用性強、便於維護、有利於擴展

三層架構分為 Controller、Service 與 Dao

  • Controller:控制層,接收前端送出的請求,對請求進行處理,並回應資料
  • Service:業務邏輯層,處理具體的業務邏輯
  • Dao:資料存取層 (Data Access Object) 或持久層,負責資料存取操作,CRUD

瀏覽器發出請求 -> Controller 接收請求、回應資料 -> Service 邏輯處理 -> Dao 資料存取

上例 Emp 程式碼可以優化成

  1. Controller
1
2
3
4
5
6
7
8
9
@RestController
public class EmpController {
    @RequestMapping("/listEmp")
    public Result listEmp(){
        EmpService empServiceA = new EmpServiceA();
        List<Emp> empList = empServiceA.listEmp();
        return Result.success(empList);
    }
}
  1. Service
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// interface
public interface EmpService {
    public List<Emp> listEmp();
}

public class EmpServiceA implements EmpService {
    private EmpDao empDao = new EmpDaoA();
    public List<Emp> listEmp(){
        List<Emp> empList = empDao.listEmp();
        empList.forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("講師");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就業指導");
            }
        });
        return empList;
    }
}
  1. Dao
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// interface
public interface EmpDao {
    public List<Emp> listEmp();
}

public class EmpDaoA implements EmpDao {
    public List<Emp> listEmp(){
        String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
        List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
        return empList;
    }
}

分層解耦

內聚:軟體中各個功能模組內部的功能聯繫

耦合:衡量軟體中各個層/模組之間的依賴、關聯的程度

軟體設計原則:高內聚低耦合

具體請見 https://blog.yexca.net/archives/145

例如上例三層架構,Controller 與 Service 耦合,Service 與 Dao 耦合

在 Controller 中直接建立 Service 物件 EmpService empServiceA = new EmpServiceA(); 使用 A,若變更為 B 則需要修改 Controller,為了更換 Service 而不修改 Controller,可以建立一個容器,使得 Controller 從容器取得物件 (依賴注入) ,Service 把服務注入容器 (控制反轉)

  • 控制反轉:Inverse Of Control,簡稱 IOC。物件的建立控制權由程式自身轉移到外部 (容器)
  • 依賴注入:Dependency Injection,簡稱 DI。容器為應用程式提供執行時所需的資源
  • Bean 物件:IOC 容器中建立、管理的物件

IOC

要把某個物件交給 IOC 容器管理,需要在對應類別上加上下列其中一個註解

註解說明位置
@Component宣告 Bean 的基礎註解不屬於下列三種類別時使用此註解 (工具類別)
@Controller@Component 的衍生註解標註在控制器類別上
@Service@Component 的衍生註解標註在業務類別上
@Repository@Component 的衍生註解標註在資料存取類別上 (MyBatis 整合時,較少用)
  • 宣告 Bean 時,可以透過 value 屬性指定名稱,若無,預設類別名稱首字母小寫
  • 在 SpringBoot 整合網頁開發中,宣告控制器 Bean 只能用 @Controller

Bean 元件掃描

上面宣告 Bean 的四個註解,若要生效,還需被元件掃描註解 @ComponentScan 掃描

此註解實際上已包含在啟動類別宣告註解 @SpringBootApplication 中,預設的掃描範圍是啟動類別所在的套件及其子套件

透過 value 或 basePackage 屬性指定掃描範圍

1
@ComponentScan({"dao","net.yexca"})

範例

上例中

  1. Controller

由於 @RestController 註解已包含 @Controller,無須修改

  1. Service
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// interface
public interface EmpService {
    public List<Emp> listEmp();
}

// 在實作類別新增
@Service // 將當前類別交給IOC容器管理,成為IOC容器中的Bean
public class EmpServiceA implements EmpService {
    private EmpDao empDao = new EmpDaoA();
    public List<Emp> listEmp(){
        List<Emp> empList = empDao.listEmp();
        empList.forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("講師");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就業指導");
            }
        });
        return empList;
    }
}
  1. Dao
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// interface
public interface EmpDao {
    public List<Emp> listEmp();
}

// 在實作類別新增
@Repository // 將當前類別交給IOC容器管理,成為IOC容器中的Bean
public class EmpDaoA implements EmpDao {
    public List<Emp> listEmp(){
        String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
        List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
        return empList;
    }
}

DI

使用 @Autowired 註解可以注入依賴,不過預設是按照型別進行,如果存在多個相同型別的 Bean 會報錯

將以上範例加上 DI (修改的程式碼將被註解)

  1. Controller
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@RestController
public class EmpController {
    @Autowired // 執行時,IOC容器會提供該型別的Bean物件,並指派給該變數 -- 依賴注入
    private EmpService empService;
    
    @RequestMapping("/listEmp")
    public Result listEmp(){
        
        //EmpService empServiceA = new EmpServiceA();
        //List<Emp> empList = empServiceA.listEmp();
        List<Emp> empList = empService.listEmp();
            
        return Result.success(empList);
    }
}
  1. Service
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// interface
public interface EmpService {
    public List<Emp> listEmp();
}

// 在實作類別新增
@Service // 將當前類別交給IOC容器管理,成為IOC容器中的Bean
public class EmpServiceA implements EmpService {
    
    @Autowired // 執行時,IOC容器會提供該型別的Bean物件,並指派給該變數 -- 依賴注入
    private EmpDao empDao;
    // private EmpDao empDao = new EmpDaoA();
    
    public List<Emp> listEmp(){
        List<Emp> empList = empDao.listEmp();
        empList.forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("講師");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就業指導");
            }
        });
        return empList;
    }
}
  1. Dao

沒有資料注入,無須修改

@Primary

若將 Service 新增一個實作類別 EmpServiceB 且也使用了 @Autowired,程式會報錯,可以透過 @Primary 註解指定使用哪一個

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// interface
public interface EmpService {
    public List<Emp> listEmp();
}

// 在實作類別新增
@Service // 將當前類別交給IOC容器管理,成為IOC容器中的Bean
public class EmpServiceA implements EmpService {
    
    @Autowired // 執行時,IOC容器會提供該型別的Bean物件,並指派給該變數 -- 依賴注入
    private EmpDao empDao;
    // private EmpDao empDao = new EmpDaoA();
    
    public List<Emp> listEmp(){
        List<Emp> empList = empDao.listEmp();
        empList.forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("講師");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就業指導");
            }
        });
        return empList;
    }
}

@Primary // 使用此 Bean
@Service // 將當前類別交給IOC容器管理,成為IOC容器中的Bean
public class EmpServiceB implements EmpService {
    
    @Autowired // 執行時,IOC容器會提供該型別的Bean物件,並指派給該變數 -- 依賴注入
    private EmpDao empDao;
    // private EmpDao empDao = new EmpDaoA();
    
    public List<Emp> listEmp(){
        List<Emp> empList = empDao.listEmp();
        empList.forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("講師");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就業指導");
            }
        });
        return empList;
    }
}

@Qualifier

指定使用哪個 Bean,在 Controller 使用,將上方 @Primary 註解取消

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@RestController
public class EmpController {
    
    @Qualifier("empServiceA") // 使用 Bean empServiceA
    @Autowired // 執行時,IOC容器會提供該型別的Bean物件,並指派給該變數 -- 依賴注入
    private EmpService empService;
    
    @RequestMapping("/listEmp")
    public Result listEmp(){
        
        //EmpService empServiceA = new EmpServiceA();
        //List<Emp> empList = empServiceA.listEmp();
        List<Emp> empList = empService.listEmp();
            
        return Result.success(empList);
    }
}

@Resource

與 @Qualifier 類似,不過不使用 @Autowired

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@RestController
public class EmpController {
    
    // @Qualifier("empServiceA") // 使用 Bean empServiceA
    // @Autowired 
    @Resource(name = "empServiceB") // 使用 Bean empServiceB
    private EmpService empService;
    
    @RequestMapping("/listEmp")
    public Result listEmp(){
        
        //EmpService empServiceA = new EmpServiceA();
        //List<Emp> empList = empServiceA.listEmp();
        List<Emp> empList = empService.listEmp();
            
        return Result.success(empList);
    }
}

@Autowired 與 @Resource 差異

@Autowired 是 Spring 框架提供的註解,而 @Resource 是 JDK 提供的註解

@Autowired 預設是按照型別注入,而 @Resource 預設是按照名稱注入