這是每個Java開發人員都應該知道的最重要的Spring注解。感謝優銳課老師對本文提供的一些幫助。
隨着越來越多的功能被打包到單個應用程序或一組應用程序中,現代應用程序的復雜性從未停止增長。盡管這種增長帶來了一些驚人的好處,例如豐富的功能和令人印象深刻的多功能性,但它要求開發人員使用越來越多的范例和庫。為了減少開發人員的工作量以及開發人員必須記住的信息量,許多Java框架都轉向了注解。
特別是Spring,它以注解的使用而聞名,它使開發人員僅用少數幾個注解就可以創建完整的表示狀態轉移(REST)應用程序編程接口(APIs)。這些注解減少了執行基本功能所需的樣板代碼量,但也可以掩蓋幕后發生的事情。例如,對字段應用依賴項注入(DI)注釋如何導致在運行時注入特定的bean?或者,REST批注如何知道綁定到哪個URL路徑?
盡管這些問題似乎是特定於Spring的(這引出了為什么非Spring開發人員需要知道對他們的答案的問題),但它們的影響深遠,令人耳目一新。根據Baeldung進行的2018年調查,有90.5%的參與者使用的是Spring。此外,根據2019年Stackoverflow開發人員調查,接受調查的所有開發人員中有16.2%使用Spring,有65.6%的人表示他們喜歡Spring。Spring的普遍存在意味着即使使用其他框架或根本不需要任何企業框架的Java開發人員也可能會遇到Spring代碼。即使是將知識僅限於Spring注解的一小部分的Spring開發人員,也會從他們的視野中受益。
在本文中,我們將深入探討Spring中可用的四個最相關的注解,特別注意注解背后的概念以及如何在較大的應用程序上下文中正確應用注解。盡管我們將詳細介紹這些注解及其相關注解,但是有關Spring注解的大量信息令人st目結舌,因此無法在本篇文章中找到。有興趣的讀者應查閱Spring的官方文檔以獲取更多詳細信息。
1. @Component
從本質上講,Spring是一個DI框架。本質上,DI框架負責以Java Bean形式將依賴項注入其他Bean中。這種范例與大多數基本應用程序相反,后者直接實例化其依賴關系。但是,在DI中,將使用間接級別創建bean,並期望DI框架為其注入依賴項。例如,一個設計良好的bean將具有一個帶有依賴項參數的構造函數——並允許DI框架傳入一個滿足該依賴關系的對象,而不是直接在構造函數中實例化該依賴關系。這種逆轉稱為控制反轉(IoC),並且是許多各種Spring庫所基於的基礎:
1 public class Bar {} 2 // The non-DI way 3 public class Foo { 4 private final Bar bar; 5 public Foo() { 6 this.bar = new Bar(); 7 } 8 } 9 // The DI way 10 public class Foo { 11 private final Bar bar; 12 public Foo(Bar bar) { 13 this.bar = bar; 14 } 15 }
DI框架要回答的最關鍵的問題之一是:哪些bean可以注入其他bean中?為了回答這個問題,Spring提供了@Component注解
。 將該注釋應用於類將通知Spring該類是一個組件,並且可以實例化該類的對象並將其注入到另一個組件中。@Component
接口通過以下方式應用於類:
1 @Component 2 public class FooComponent {}
盡管@Component注解
足以通知Spring Bean的可注入性;Spring還提供了專門的注解,可用於創建具有更有意義的上下文信息的組件。
@Service
@Service
(顧名思義)表示Bean是服務。 根據官方的@Service注解文檔:
[@Service
批注]指示帶注解的類是“服務”,最初由Domain-Driven Design(Evans,2003)定義為“作為接口提供的操作,在模型中獨立存在,沒有封裝狀態”。
可能還表明某個類是“業務服務門面”(就核心J2EE模式而言)或類似的東西。
通常,企業應用程序中服務的概念含糊不清,但是在Spring應用程序的上下文中,服務是提供與域邏輯或外部組件交互的方法而無需保持更改服務整體行為的狀態的任何類。例如,服務可以代表應用程序來從數據庫獲取文檔或從外部REST API獲取數據。
1 @Service 2 public class FooService {}
盡管沒有關於服務狀態的明確規則,但是服務通常不像域對象那樣包含狀態。例如,與將名稱,地址和社會安全號碼視為域對象的狀態的方式相同,不會將REST客戶端,緩存或連接池視為服務的狀態。實際上,由於服務的全部定義,@Service
和@Component
通常可以互換使用。
@Repository
@Service
是用於更多通用目的的,而@Repository注解
是@Component注解
的一種特殊化,它是為與數據源(例如數據庫和數據訪問對象(DAOs))進行交互的組件而設計的。
1 @Repository 2 public class FooRepository {}
根據官方的@Repository
文檔:
指示帶注解的類是“存儲庫”,最初由Domain-Driven Design(Evans,2003)定義為“一種封裝存儲,檢索和搜索行為的機制,該機制模仿對象的集合”。
實現諸如“數據訪問對象”之類的傳統Java EE模式的團隊也可以將這種構造型應用於DAO類,盡管在這樣做之前應注意理解數據訪問對象和DDD樣式存儲庫之間的區別。此注解是通用的刻板印象,各個團隊可以縮小其語義並適當使用。
除了將特定的類標記為處理數據源的組件之外,Spring框架還將對@Repository注解
的bean進行特殊的異常處理。 為了維護一致的數據接口,Spring可以將本機存儲庫引發的異常(例如SQL或Hibernate實現)轉換為可以統一處理的常規異常。 為了包括用@Repository注解
的類的異常翻譯,我們實例化了PersistenceExceptionTranslationPostProcessor類型的bean(我們將在后面的部分中看到如何使用@Configuration
和@Bean注解
):
1 @Configuration 2 public class FooConfiguration { 3 @Bean 4 public PersistenceExceptionTranslationPostProcessor exceptionTranslator() { 5 return new PersistenceExceptionTranslationPostProcessor() 6 } 7 }
包括該bean將通知Spring尋找PersistenceExceptionTranslator的所有實現,並在可能的情況下使用這些實現將本機RuntimeException
轉換為DataAccessExceptions
。有關使用@Repository注解
進行異常轉換的更多信息,請參見官方的Spring Data Access文檔。
@Controller
@Component注解
的最后一個專業化可以說是三人組中最常用的。Spring Model-View-Controller(MVC)是Spring Framework最受歡迎的部分之一,它使開發人員可以使用@Controller注解輕松創建REST API。該注解在應用於類時,指示Spring框架將該類視為應用程序的Web界面的一部分。
通過將@RequestMapping注解
應用於該類的方法來在此類中創建端點——其中@RequestMapping注解
的值是路徑(相對於API端點綁定到的控制器的根路徑),並且 method是終結點綁定到的超文本傳輸協議(HTTP)方法。例如:
1 @Controller 2 public class FooController { 3 @RequestMapping(value = "/foo", method = RequestMethod.GET) 4 public List<Foo> findAllFoos() { 5 // ... return all foos in the application ... 6 } 7 }
這將創建一個端點,該端點在/foo
路徑上偵聽GET
請求,並將所有Foo
對象的列表(默認情況下表示為JavaScript Object Notation(JSON)列表)返回給調用方。例如,如果Web應用程序在https://localhost
上啟動,則端點將綁定到https://localhost/foo
。我們將在下面更詳細地介紹@RequestMapping注解
,但是就目前而言,足以知道 @Controller注解
是Spring框架的重要組成部分,並且它指示Spring框架創建大型而復雜的Web服務實現。
@ComponentScan
如在Java中創建注解中所述,注解本身不會執行任何邏輯。相反,注解只是標記,它們表示有關構造的某些信息,例如類,方法或字段。為了使注釋有用,必須對其進行處理。對於@Component注解
及其專業化,Spring不知道在哪里可以找到所有使用@Component注解
的類。
為此,我們必須指示Spring應該掃描類路徑上的哪些包。在掃描過程中,Spring DI Framework處理提供的包中的每個類,並記錄所有用@Component
或@Component
特化注解的類。掃描過程完成后,DI框架就會知道哪些類適合進行注入。
為了指示Spring掃描哪些軟件包,我們使用@ComponentScan注解
:
1 @Configuration 2 @ComponentScan 3 public class FooConfiguration { 4 // ... 5 }
在后面的部分中,我們將深入研究@Configuration注解
,但就目前而言,足以知道@Configuration注解
指示Spring批注的類提供了可供DI框架使用的配置信息。默認情況下(如果沒有為@ComponentScan注解
提供任何參數)將掃描包含配置的包及其所有子包。要指定一個包或一組包,請使用basePackages
字段:
1 @Configuration 2 @ComponentScan(basePackages = "com.example.foo") 3 public class FooConfiguration { 4 // ... 5 }
在上面的示例中,Spring將掃描com.example.foo
軟件包及其所有子軟件包中的合格組件。如果僅提供一個基本軟件包,則@ComponentScan注解
可以簡化為@ComponentScan("com.example.foo")
。如果需要多個基本軟件包,則可以為basePackages
字段分配一組字符串:
1 @Configuration 2 @ComponentScan(basePackages = {"com.example.foo", "com.example.otherfoo"}) 3 public class FooConfiguration { 4 // ... 5 }
2. @Autowired
對於任何DI框架,第二個至關重要的問題是:創建bean時必須滿足哪些依賴關系?為了通知Spring框架我們期望將哪些字段或構造函數參數與依賴項一起注入或連接,Spring提供了@Autowired
annotation。此注解通常適用於字段或構造函數——盡管也可以將其應用於設置方法(這種用法不太常見)。
當應用於字段時,即使沒有設置器,Spring也會在創建時將符合條件的依賴項直接注入到字段中:
1 @Component 2 public class FooComponent { 3 @Autowired 4 private Bar bar; 5 }
這是將依賴項注入組件的便捷方法,但是在測試類時確實會產生問題。例如,如果我們要編寫一個執行FooComponent
類的測試夾具,而沒有在夾具中包括Spring測試框架,那么我們將無法在bar
字段中注入模擬Bar
值(而無需執行繁瑣的反射)。我們可以將@Autowired注解
添加到接受Bar
參數並將其分配給bar
字段的構造函數中:
1 @Component 2 public class FooComponent { 3 private final Bar bar; 4 @Autowired 5 public Foo(Bar bar) { 6 this.bar = bar; 7 } 8 }
這仍然使我們可以使用模擬Bar
實現直接實例化FooComponent
類的對象,而不會給Spring測試配置增加負擔。例如,以下將是有效的JUnit測試用例(使用Mockito進行模擬):
1 public class FooTest { 2 @Test 3 public void exerciseSomeFunctionalityOfFoo() { 4 Bar mockBar = Mockito.mock(Bar.class); 5 FooComponent foo = new FooComponent(mockBar); 6 // ... exercise the FooComponent object ... 7 }
使用@Autowired注解
構造函數還允許我們在將注入的Bar
bean分配給bar
字段之前對其進行訪問和操作。 例如,如果我們要確保注入的Bar
Bean永遠不會為null
,則可以在將提供的Bar Bean分配給bar
字段之前執行此檢查:
1 @Component 2 public class FooComponent { 3 private final Bar bar; 4 @Autowired 5 public FooComponent(Bar bar) { 6 this.bar = Objects.requireNonNull(bar); 7 } 8 } 9
@Qualifier
在某些情況下,可能有多個候選關系。這給Spring帶來了一個問題,因為它必須在創建組件時決定要注入哪個特定的bean,否則,如果無法確定單個候選對象,它將失敗。例如,以下代碼將引發 NoUniqueBeanDefinitionException
:
1 public interface FooDao { 2 public List<Foo> findAll(); 3 } 4 @Repository 5 public class HibernateFooDao implements FooDao { 6 @Override 7 public List<Foo> findAll() { 8 // ... find all using Hibernate ... 9 } 10 } 11 @Repository 12 public class SqlFooDao implements FooDao { 13 @Override 14 public List<Foo> findAll() { 15 // ... find all using SQL ... 16 } 17 } 18 @Controller 19 public class FooController { 20 private final FooDao dao; 21 @Autowired 22 public FooController(FooDao dao) { 23 this.dao = dao; 24 } 25 }
Spring不知道是否要注入HibernateDooDao
或SqlFooDao
,因此會拋出致命的NoUniqueBeanDefinitionException
。為了幫助Spring解決選擇哪個bean,我們可以使用@Qualifier注解
。通過為@Qualifier注解
提供與@Component注解
(或其任何專業化)提供的名稱相匹配的鍵,以及@Autowired注解
,我們可以縮小合格的注入候選對象的范圍。例如,在以下代碼段中,將HibernateFooDao
注入到FooController
中,並且不會引發NoUniqueBeanDefinitionException
:
1 public interface FooDao { 2 public List<Foo> findAll(); 3 } 4 @Repository("hibernateDao") 5 public class HibernateFooDao implements FooDao { 6 @Override 7 public List<Foo> findAll() { 8 // ... find all using Hibernate ... 9 } 10 } 11 @Repository("sqlDao") 12 public class SqlFooDao implements FooDao { 13 @Override 14 public List<Foo> findAll() { 15 // ... find all using SQL ... 16 } 17 } 18 @Controller 19 public class FooController { 20 private final FooDao dao; 21 @Autowired 22 @Qualifier("hibernateDao") 23 public FooController(FooDao dao) { 24 this.dao = dao; 25 } 26 }
3. @Configuration
由於Spring框架的巨大規模-處理從DI到MVC到事務管理的所有內容,因此需要開發人員提供的配置級別。例如,如果我們希望定義一組可用於自動裝配的Bean(例如上面看到的PersistenceExceptionTranslationPostProcessor Bean),則必須告知Spring一些配置機制。Spring通過適當命名的@Configuration注解
提供了這種機制。當將此注解應用於類時,Spring將該類視為包含可用於參數化框架的配置信息的類。根據官方的Spring @Configuration
文檔:
指示一個類聲明了一個或多個@Bean
方法,並且可以由Spring容器進行處理以在運行時為這些bean生成bean定義和服務請求,例如:
@Bean
正如我們在上面看到的,我們可以手動創建Spring將包含的新bean作為注入的候選對象,而無需注解類本身。當我們無法訪問該類的源代碼或者該類存在於不屬於組件掃描過程的軟件包中時,可能就是這種情況。在上面的@Qualifier
示例中,我們也可以放棄@Repository
annotations並在帶有@Configuration
注釋的類中使用@Bean注解
,以指示Spring在需要FooDao
時使用HibernateFooDao
:
1 public interface FooDao { 2 public List<Foo> findAll(); 3 } 4 public class HibernateFooDao implements FooDao { 5 @Override 6 public List<Foo> findAll() { 7 // ... find all using Hibernate ... 8 } 9 } 10 public class SqlFooDao implements FooDao { 11 @Override 12 public List<Foo> findAll() { 13 // ... find all using SQL ... 14 } 15 } 16 @Configuration 17 public class FooConfiguration { 18 @Bean 19 public FooDao fooDao() { 20 return new HibernateFooDao(); 21 } 22 }
使用此配置,Spring現在將具有在請求FooDao
時實例化HibernateDooDao
所需的邏輯。本質上,我們創建了一個Factory方法,框架可以在需要時使用該方法來實例化FooDao
的實例。如果在創建bean時排除了@Autowired
參數,我們可以通過向使用@Bean注解
的方法中添加參數來反對這種依賴性。如果我們用@Component
或@Component
的任何特化來注解組件,Spring會在創建組件時知道注入依賴項,但是由於我們是在Spring Framework外部直接調用構造函數,因此必須提供依賴項。例如:
1 @Component 2 public class Bar {} 3 public class FooComponent { 4 private final Bar bar; 5 @Autowired 6 public FooComponent(Bar bar) { 7 this.bar = bar; 8 } 9 } 10 @Configuration 11 public class FooConfiguration { 12 @Bean 13 public FooComponent fooComponent(Bar bar) { 14 return new FooComponent(bar); 15 } 16 }
Spring尋找滿足fooComponent
方法參數的已注冊候選者,當找到一個候選者時,它將被傳入並最終傳遞給FooComponent
構造函數。請注意,任何使用@Component注解
或任何特殊化注解的bean或使用其他@Bean
method創建的bean都可以注入@Bean
方法參數中。例如:
1 public class Bar {} 2 public class FooComponent { 3 private final Bar bar; 4 @Autowired 5 public FooComponent(Bar bar) { 6 this.bar = bar; 7 } 8 } 9 @Configuration 10 public class FooConfiguration { 11 @Bean 12 public Bar bar() { 13 return new Bar(); 14 } 15 @Bean 16 public FooComponent fooComponent(Bar bar) { 17 return new FooComponent(bar); 18 } 19 }
請注意,使用@Bean注解方法的慣例與@Bean
相同,首字母小寫。例如,如果我們要創建一個FooComponent
,則用於創建bean(並用@Bean注解
)的方法通常稱為fooComponent
.。
4. @RequestMapping
@Controller注解
的大部分功能都來自@RequestMapping注解
,該注解指示Spring創建一個映射到帶注解方法的Web終結點。創建Web API時,框架需要知道如何處理對特定路徑的請求。例如,如果對https://localhost/foo
進行了HTTP GET
調用,Spring需要知道如何處理該請求。此綁定(或映射)過程是@RequestMapping注解
的權限,該注解通知Spring應該將特定的HTTP動詞和路徑映射到特定的方法。例如,在上一節中,我們看到我們可以指示Spring使用以下代碼段將HTTP GET
映射到/ foo
:
1 @Controller 2 public class FooController { 3 @RequestMapping(value = "/foo", method = RequestMethod.GET) 4 public List<Foo> findAll() { 5 // ... return all foos in the application ... 6 } 7 }
請注意,可以將多個HTTP動詞提供給method參數,但這在實踐中是異常的。 由於幾乎總是將單個HTTP動詞提供給method參數——並且這些動詞通常最終以GET
, POST
, PUT
, 和 DELETE
結尾,因此Spring還包括四個附加注解,可用於簡化@RequestMapping
方法的創建:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
如果需要根路徑(即與控制器路徑匹配的路徑),則不需要value參數。@RequestMapping注解
也可以應用於控制器本身,該控制器設置整個控制器的根路徑。例如,以下控制器在/foo
路徑中創建一個GET
端點,在/foo/bar
中創建另一個POST
端點:
1 @Controller 2 @RequestMapping("/foo") 3 public class FooController { 4 @GetMapping 5 public List<Foo> findAll() { 6 // ... return all foos in the application ... 7 } 8 @PostMapping("/bar") 9 public void doSomething() { 10 // ... do something ... 11 } 12 }
@PathVariable
在某些情況下,可能會在路徑中提供路徑變量,這是正確處理請求所必需的。若要獲取此路徑變量的值,可以向使用@RequestMapping注解
的方法提供參數,並且可以將@PathVariable注解
應用於此參數。例如,如果需要實體的ID來刪除它,則可以將該ID作為路徑變量提供,例如對/foo/1
的DELETE
請求。為了捕獲提供給負責處理DELETE
請求的方法的1
,我們捕獲路徑變量,方法是用大括號將變量名括起來,並為處理程序方法的參數應用@PathVariable注解
,其中將值提供給@PathVariable
匹配路徑中捕獲的變量的名稱:
1 @Controller 2 public class FooController { 3 @DeleteMapping("/foo/{id}") 4 public void deleteById(@PathVariable("id") String id) { 5 // ... delete Foo with ID "id" ... 6 } 7 }
默認情況下,假定@PathVariable
的名稱與帶注解的參數的名稱匹配,因此,如果參數的名稱與路徑中捕獲的變量的名稱完全匹配,則無需為@PathVariable注解
提供任何值:
1 @Controller 2 public class FooController { 3 @DeleteMapping("/foo/{id}") 4 public void deleteById(@PathVariable String id) { 5 // ... delete Foo with ID "id" ... 6 } 7 }
Spring將嘗試將捕獲的路徑變量強制轉換為以@PathVariable注解
的參數的數據類型。例如,如果我們將ID path變量的值除為整數,則可以將id參數的數據類型更改為int
:
1 @Controller 2 public class FooController { 3 @DeleteMapping("/foo/{id}") 4 public void deleteById(@PathVariable int id) { 5 // ... delete Foo with ID "id" ... 6 } 7 }
如果在路徑中提供了諸如字符串baz
之類的值(即/foo/baz
),則會發生錯誤。
@RequestParam
除了捕獲路徑變量之外,我們還可以使用@RequestParam注解
捕獲查詢參數。@RequestParam
以與@PathVariable注解
相同的方式將參數裝飾到處理程序方法,但是提供給@RequestParam
annotation的值與查詢參數的鍵匹配。例如,如果我們希望對/foo?limit=100
的路徑進行HTTP GET
調用,則可以創建以下控制器來捕獲限制值:
1 @Controller 2 public class FooController { 3 @GetMapping("/foo") 4 public List<Foo> findAll(@QueryParam("limit") int limit) { 5 // ... return all Foo objects up to supplied limit ... 6 } 7 }
與@PathVariable
一樣,可以省略提供給@RequestParam注解
的值,並且默認情況下將使用參數的名稱。同樣,如果可能的話,Spring將把捕獲的查詢參數的值強制轉換為參數的類型(在上述情況下為int
)。
@RequestBody
在調用中提供請求正文的情況下(通常通過創建或更新條目的POST
或PUT
調用完成),Spring提供了@RequestBody注解
。與前兩個注解一樣,@RequestBody注解
應用於處理程序方法的參數。 然后,Spring會將提供的請求主體反序列化為參數的類型。例如,我們可以使用具有類似於以下內容的請求主體的HTTP調用創建新的Foo
:
1 {"name": "some foo", "anotherAttribute": "bar"}
然后,我們可以創建一個包含與期望的請求主體匹配的字段的類,並創建一個捕獲該請求主體的處理程序方法:
1 public class FooRequest { 2 private String name; 3 private String anotherAttribute; 4 public void setName(String name) { 5 this.name = name; 6 } 7 public String getName() { 8 return name; 9 } 10 public void setAnotherAttribute(String anotherAttribute) { 11 this.anotherAttribute = anotherAttribute; 12 } 13 public String getAnotherAttribute() { 14 return anotherAttribute; 15 } 16 } 17 @Controller 18 public class FooController { 19 @PostMapping("/foo") 20 public void create(@RequestBody FooRequest request) { 21 // ... create a new Foo object using the request body ... 22 } 23 }
結論
盡管有許多Java框架,但Spring卻是無處不在的,它是最普遍的一種。 從REST API到DI,Spring包括豐富的功能集,這些功能使開發人員無需編寫大量樣板代碼即可創建復雜的應用程序。 Spring提供的一種機制是注解,它使開發人員可以修飾類和方法,並為它們提供上下文信息,Spring框架可以使用這些信息來代表我們創建組件和服務。由於Spring的普遍性,每個Java開發人員都可以從理解這些Spring注解以及它們在實踐中的應用中受益匪淺。