这是每个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注解以及它们在实践中的应用中受益匪浅。