面試官:Spring Boot 控制層中,@Service 可以完全替代 @Controller 嗎?90% 都會答錯!

Java技术栈發表於2024-10-30

作者:毅航
來源:juejin.cn/post/7393533304505204787

在SpringBoot開發中,@Controller@Service基本上是日常開發中使用的最頻繁的兩個註解。但你有沒考慮過@Service代替@Controller註解來標註到控制層的場景?換言之,經過@Service標註的控制層能否實現將使用者請求分發到服務層的功能?

前言

在SpringBoot開發中,@Controller註解用於標識一個控制器類,該類負責處理Web請求。而控制器類通常包含若干個方法,每個方法對應一個HTTP請求的處理邏輯。而控制器是MVC(Model-View-Controller)架構的一部分,其主要負責將使用者請求分發到適當的服務層,並返回檢視或響應資料。而@Service註解用於標識一個服務類,用以負責處理業務邏輯和與資料訪問層互動。

相信對於大多數Java開發者來說@Controller@Service註解的使用都不算太難。但進一步,用@Service標註控制層能否達到和@Controller註解相同的功能呢?對於這個操作你可能會覺得很瘋狂,並下意識的說出不可能。但事實果真如此嗎?

我們不妨先透過一個簡單的例子來驗證一下。

@Sercice代替@Controller

我們首先自定義一個ServiceController的控制層,其內部透過Autowired註解注入一個UserMapper,並透過userMapper來實現控制層與資料層的互動。

Spring Boot 基礎就不介紹了,推薦看這個實戰專案:

https://github.com/javastacks/spring-boot-best-practice

具體程式碼如下:

ServiceController
@Service
@RequestMapping("/ts")
public class ServiceController {

    @Autowired
    private UserMapper userMapper;

    @GetMapping("get-services")
    @ResponseBody
    public User getServices() {
        User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class)
                .eq(User::getUsername, "zhangSan"));
        return user;
    }
}

然後,透過PostMan傳送一個Get請求,以請求 http://localhost:8080/ts/get-services 其返回內容如下 :

Date: Sun, 21 Jul 2024 02:37:39 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "username": "zhangSan",
  "id": 1,
  "type": null,
  "remark": "test1"
}

透過返回內容,不難看出我們的請求順利到ServiceControllergetServices方法。也就是說我們完全可以用@Service來替代@Controller標註在控制層上!

揭秘背後原理

你可能會覺得@Service來替代@Controller這樣的操作有的反常規,因為在學習SpringBoot時,從來也沒有那個教程告訴我們@Service註解還有這樣的騷操作。那@Service可以這樣使用的背後原因到底是什麼呢?

眾所周知,@Service@Controller註解都能被Spring容器所載入,並注入到Spring容器中。我們以SpringBoot應用為例來分析其注入容器的全過程。

在分析之前我們首先明確一點,對於Spring而言其會根據配置(如 XML 檔案或 @ComponentScan 註解)掃描指定的包及其子包,查詢標記有 @Controller@Service@Repository@Component 的類。但對於Springboot應用而言,其並沒有顯示的使用XmL配置或@ComponentScan指定掃描路徑的方式來載入對應路徑下的Bean資訊。

這背後的原因主要在於@SpringBootApplication 註解的使用,@SpringBootApplication其實是一個組合註解,其內部包括以下三個註解:

  • @EnableAutoConfiguration: 啟用 Spring Boot 的自動配置機制。
  • @ComponentScan: 啟用元件掃描,以便自動發現並註冊 Spring 元件。
  • @Configuration: 表示這是一個 Spring 配置類。

在預設情況下,Spring Boot 會從主應用類所在的包開始進行元件掃描,這意味著只要 @Controller@Service 註解的類位於主應用類所在包及其子包中,它們就會被自動發現並註冊到 Spring 容器中。

明白了SpringBoot對於Bean的載入邏輯後,我們再來深入到其內部來看。SpringBoot對於這部分Bean的載入流程如圖所示:

當我們在main方法中執行SpringApplication.run時,其在run方法內容會完成Spring容器的建立,以及Bean的載入。具體來看,其在AbstractApplicationContext中的invokeBeanPostBeanFactoryPostProcessore時,會透過 ConfigurationClassPostProcessorpostProcessBeanDefinitionRegistry 方法載入路徑下所有的bean名稱資訊,然後在finshBeanFactoryInitialization完成bean的例項化。而我們繞了這麼一大圈就是皆在說明,被@Service@Controller所標註的類在SpringBoot框架中是如何一步步被注入到容器的。

進一步,筆者曾在揭秘@Controller內部方法與URL繫結的全流程中談到,對於控制層內的方法,其會在AbstractHandlerMethodMapping中的 afterPropertiesSet()完成請求url與方法的對映繫結。更進一步,afterPropertiesSet的處理邏輯全部委託于于 getCandidateBeanNamesprocessCandidateBean兩個方法。

getCandidateBeanNames 會獲取當前容器中所有的bean 的名稱集合,並篩選類中標有@Controller或者 @RequestMapping註解的類交給processCandidateBean處理,以完成請求url與方法的對映。

此時,我們不妨來回看我們一開始的ServiceController的樣例程式碼:

@Service
@RequestMapping("/ts")
public class ServiceController {

   // ....省略內部細節資訊
}

不難發現,其雖然沒使用@Controller修飾,但其卻被@RequestMapping註解資訊,也就是說其能被processCandidateBean所處理,進而其也就能完成url和方法對映關係的維護。更進一步,當請求至DispatcherServlet時,SpringMVC變更透過其url資訊,找到能處理相應請求的HandlerMethod。從而也就能完成url的處理以及檢視的渲染。

事實上,只要你能確保Bean資訊注入到容器,並且類資訊上至少有@Controller或者 @RequestMapping註解,那便能在AbstractHandlerMethodMapping中所解析,然後完成url與方法的繫結!這也就是為什麼我們一開始花費精力研究@Controller@Service 注入容器的原因。

總結

在 SpringBoot 開發中,@Controller@Service 是最常用的註解。通常,@Controller 用於標識控制器類,處理 Web 請求並將請求分發到服務層;而 @Service 用於標識服務類,處理業務邏輯。然而,如果用 @Service 來標註控制層是否可以實現與 @Controller 相同的功能呢?

答案是肯定的。只要類被 @Service 標註,並且包含 @RequestMapping 等註解,Spring Boot 依然能夠將其作為控制器來處理請求並返回響應。這背後的原理在於 @Service@Controller 都能被 Spring 容器載入和註冊,並且 @RequestMapping 註解能夠使類的方法與 URL 對映,從而實現請求處理功能。

更多文章推薦:

1.Spring Boot 3.x 教程,太全了!

2.2,000+ 道 Java面試題及答案整理(2024最新版)

3.免費獲取 IDEA 啟用碼的 7 種方式(2024最新版)

覺得不錯,別忘了隨手點贊+轉發哦!

相關文章