每个Java开发人员都应该知道的4个Spring注释


随着越来越多的功能被打包到单个应用程序或一组应用程序中,现代应用程序的复杂性永无止境地增长。尽管这种增长带来了一些神奇的好处,例如丰富的功能和令人印象深刻的多功能性,但它要求开发人员使用越来越多的范例和库。为了减少开发人员的工作量以及开发人员必须记住的信息量,许多Java框架都转向了注释。

特别是Spring,它以注释的使用而闻名,它使开发人员仅用少数几个注释就可以创建整个表示状态转移(REST)应用程序编程接口(API)。这些注释减少了执行基本功能所需的样板代码量,但也可能掩盖了幕后发生的事情。例如,对字段应用依赖项注入(DI)注释如何导致在运行时注入特定的bean?或者,REST批注如何知道要绑定到哪个URL路径?

尽管这些问题似乎是特定于Spring的(这引出了为什么非Spring开发人员需要知道对他们的答案的问题),但它们的影响深远,令人耳目一新。根据Baeldung进行的2018年调查,90.5%的参与者使用的是Spring。此外,根据2019#2开发者的调查,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库所基于的基础:

public class Bar {}

// The non-DI way
public class Foo {

    private final Bar bar;

    public Foo() {
        this.bar = new Bar();
    }
}

// The DI way
public class Foo {

    private final Bar bar;

    public Foo(Bar bar) {
        this.bar = bar;
    }
}

其中的一个DI框架来回答是最关键的问题:哪些豆考生将被注入其他bean?为了回答这个问题,Spring提供了@Component注释。将此注释应用于类将通知Spring该类是一个组件,并且可以实例化该类的对象并将其注入到另一个组件中。该@Component接口通过以下方式应用于类:

@Component
public class FooComponent {}

尽管@Component注释足以通知Spring Bean的可注入性;Spring还提供了专门的注释,可用于创建具有更有意义的上下文信息的组件。

@Service@Service注解-顾名思义-表示一个bean是一种服务。根据官方@Service注释文档:

[ @Service注解]指示带注释的类是“服务”,最初由域驱动设计(Evans,2003)定义为“作为接口提供的操作,在模型中独立存在,没有封装状态”。

可能还表明某个类是“业务服务门面”(就核心J2EE模式而言)或类似的东西。 通常,企业应用程序中的服务概念含糊不清,但是在Spring应用程序的上下文中,服务是提供与域逻辑或外部组件进行交互而无需保持更改服务整体行为状态的任何类。例如,服务可以代表应用程序来从数据库获取文档或从外部REST API获取数据。

@Service
public class FooService {}

尽管没有关于服务状态的明确规则,但是服务通常不像域对象那样包含状态。例如,与将名称,地址和社会安全号码视为域对象的状态的方式相同,不会将REST客户端,缓存或连接池视为服务的状态。在实践中,@Service并@Component经常互换使用,由于服务的全方位的定义。

@Repository 虽然注解是@Service为了更一般的目的,但@Repository注解是注解的一种特殊化,@Component它是为与数据源(例如数据库和数据访问对象(DAO))进行交互的组件而设计的。

@Repository
public class FooRepository {}

根据官方@Repository文件:

指示带注释的类是“存储库”,最初由Domain-Driven Design(Evans,2003)定义为“一种封装存储,检索和搜索行为的机制,该机制模仿对象的集合”。

实现诸如“数据访问对象”之类的传统Java EE模式的团队也可以将此构造型应用于DAO类,尽管在这样做之前应注意理解数据访问对象和DDD样式存储库之间的区别。此注释是通用的刻板印象,各个团队可以缩小其语义并适当使用。 除了将特定的类标记为处理数据源的组件之外,Spring Framework还将处理@Repository使用特殊异常处理注释的bean 。为了保持一致的数据接口,Spring可以将本机存储库引发的异常(例如SQL或Hibernate实现)转换为可以统一处理的常规异常。为了为带有注释的类包括异常转换@Repository,我们实例化了一个类型的bean PersistenceExceptionTranslationPostProcessor (我们将在后面的部分中看到如何使用@Configuration和@Bean注释):

@Configuration
public class FooConfiguration {

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslator() {
        return new PersistenceExceptionTranslationPostProcessor()
    }
}

包括该bean将通知Spring寻找的所有实现,PersistenceExceptionTranslator 并在可能的情况下 使用这些实现将native RuntimeExceptions 转换为DataAccessExceptions。有关带@Repository注释的异常转换的更多信息,请参见Spring Data Access官方文档。

@Controller

@Component注释的最后一个专业化可以说是三人组中最常用的。Spring Model-View-Controller(MVC)是Spring Framework最受欢迎的部分之一,它使开发人员可以使用@Controller注释轻松创建REST API 。该注释在应用于类时,指示Spring框架将该类视为应用程序的Web界面的一部分。

通过将@RequestMapping注释应用于该类的方法在此类中创建端点,其中@RequestMapping注释的值是路径(相对于API端点绑定到的控制器的根路径),而方法是超文本端点绑定到的传输协议(HTTP)方法。例如:

@Controller
public class FooController {

    @RequestMapping(value = "/foo", method = RequestMethod.GET)
    public List<Foo> findAllFoos() {
        // ... return all foos in the application ... 
    }
}

这将创建一个端点,该端点在/foo 路径上侦听 GET请求,并将所有 Foo 对象的列表(默认情况下表示为JavaScript Object Notation(JSON)列表)返回给调用方。例如,如果Web应用程序在上启动https://localhost,则端点将绑定到 https://localhost/foo。我们将@RequestMapping在下面更详细地介绍该批注,但是就目前而言,足以知道该@Controller批注是Spring Framework的重要组成部分,并且可以指示Spring Framework代表我们创建大型而复杂的Web服务实现。

@ComponentScan

如在Java中创建注释中所述,注释本身不会执行任何逻辑。相反,注释只是标记,它们表示有关构造的某些信息,例如类,方法或字段。为了使注释有用,必须对其进行处理。对于@Component注解及其专业化,Spring不知道在哪里可以找到所有带注解的类@Component。

为此,我们必须指示Spring应当扫描classpath上的哪些包。在扫描过程中,Spring DI Framework将处理提供的包中的每个类,并记录所有用注释@Component或特殊化的类@Component。扫描过程完成后,DI框架就会知道哪些类可以进行注入。

为了指示Spring扫描哪些软件包,我们使用@ComponentScan注释:

@Configuration
@ComponentScan
public class FooConfiguration {
    // ...
}

在后面的部分中,我们将深入研究@Configuration批注,但是暂时知道@Configuration批注指示Spring批注的类提供了DI框架要使用的配置信息就足够了。默认情况下(如果没有为@ComponentScan注释提供任何参数)将扫描包含配置的包及其所有子包。要指定一个包或一组包,请使用以下 basePackages 字段:

@Configuration
@ComponentScan(basePackages = "com.example.foo")
public class FooConfiguration {
    // ...
}

在上面的示例中,Spring将扫描软件包com.example.foo 及其所有子软件包 以查找合格的组件。如果仅提供一个基本包,则@ComponentScan注释可以简化为@ComponentScan("com.example.foo")。如果需要多个基本软件包,则basePackages可以为该 字段分配一组字符串:

@Configuration
@ComponentScan(basePackages = {"com.example.foo", "com.example.otherfoo"})
public class FooConfiguration {
    // ...
}

2. @Autowired

对于任何DI框架,第二个至关重要的问题是:创建bean时必须满足哪些依赖关系?为了告知Spring框架我们期望将哪些字段或构造函数参数与依赖项一起注入或连接 ,Spring提供了@Autowired注释。此注释通常适用于字段或构造函数-尽管也可以将其应用于设置方法(这种用法不太常见)。

当应用于字段时,即使没有设置器,Spring也会在创建时将符合条件的依赖项直接注入到字段中:

@Component
public class FooComponent {

    @Autowired
    private Bar bar;
}

这是将依赖项注入组件的便捷方法,但是在测试类时确实会产生问题。例如,如果我们要编写一个练习FooComponent 类的测试夹具,而没有在夹具中 包含Spring测试框架,那么我们将无法在Bar 该bar 字段中注入模拟 值 (而无需执行繁琐的反射)。我们可以改为将@Autowired注释添加到接受Bar 参数并将其分配给 bar 字段的构造函数中 :

@Component
public class FooComponent {

    private final Bar bar;

    @Autowired
    public Foo(Bar bar) {
        this.bar = bar;
    }
}

这仍然允许我们FooComponent 使用模拟 Bar 实现直接实例化该类的对象, 而不会给Spring测试配置带来负担。例如,以下将是有效的JUnit测试用例(使用Mockito进行模拟):

public class FooTest {

    @Test
    public void exerciseSomeFunctionalityOfFoo() {

        Bar mockBar = Mockito.mock(Bar.class);
        FooComponent foo = new FooComponent(mockBar);

        // ... exercise the FooComponent object ...
    }

使用注释构造函数@Autowired还可以使我们在将注入的Bar Bean分配给bar 字段之前对其进行 访问和操作 。例如,如果我们要确保注入的 Bar bean从不 null,则可以在将提供的Bar bean 分配给bar 字段之前执行此检查 :

@Component
public class FooComponent {

    private final Bar bar;

    @Autowired
    public FooComponent(Bar bar) {
        this.bar = Objects.requireNonNull(bar);
    }
}

@Qualifier

在某些情况下,可能有多个候选关系。这给Spring带来了一个问题,因为它必须在创建组件时决定要注入哪个特定的bean,否则就无法确定单个候选对象。例如,以下代码将抛出NoUniqueBeanDefinitionException

public interface FooDao {
    public List<Foo> findAll();
}

@Repository
public class HibernateFooDao implements FooDao {

    @Override
    public List<Foo> findAll() {
        // ... find all using Hibernate ...
    }
}

@Repository
public class SqlFooDao implements FooDao {

    @Override
    public List<Foo> findAll() {
        // ... find all using SQL ...
    }
}

@Controller
public class FooController {

    private final FooDao dao;

    @Autowired
    public FooController(FooDao dao) {
        this.dao = dao;
    }
}

Spring将不知道是否要注入 HibernateDooDaoSqlFooDao ,因此将引发致命事故NoUniqueBeanDefinitionException。为了帮助Spring解决选择哪个bean,我们可以使用@Qualifier注释。通过为@Qualifier注释提供与@Component注释一起提供的名称(或其任何专业化名称)相匹配的关键字@Autowired,我们可以缩小合格的注入候选对象的范围。例如,在以下代码段中, HibernateFooDao 将会被注入, FooController 而不会 NoUniqueBeanDefinitionException 抛出:

public interface FooDao {
    public List<Foo> findAll();
}

@Repository("hibernateDao")
public class HibernateFooDao implements FooDao {

    @Override
    public List<Foo> findAll() {
        // ... find all using Hibernate ...
    }
}

@Repository("sqlDao")
public class SqlFooDao implements FooDao {

    @Override
    public List<Foo> findAll() {
        // ... find all using SQL ...
    }
}

@Controller
public class FooController {

    private final FooDao dao;

    @Autowired
    @Qualifier("hibernateDao")
    public FooController(FooDao dao) {
        this.dao = dao;
    }
}
  1. @配置
  2. 由于Spring框架的巨大规模-处理从DI到MVC到事务管理的所有内容,因此需要开发人员提供的配置级别。例如,如果我们希望定义一组可用于自动装配的PersistenceExceptionTranslationPostProcessor Bean (例如上面看到的 Bean),则必须使用某种配置机制通知Spring。Spring通过适当命名的@Configuration注释提供了这种机制。当将此注释应用于类时,Spring将该类视为包含可用于参数化框架的配置信息的类。根据Spring@Configuration的官方文档:

指示一个类声明了一个或多个 @Bean方法,并且可以由Spring容器进行处理以在运行时为这些bean生成bean定义和服务请求,例如:

@Bean

正如我们在上面看到的,我们可以手动创建Spring将包含的新bean作为注入的候选对象,而无需注释类本身。当我们无法访问该类的源代码或者该类存在于不属于组件扫描过程的软件包中时,可能就是这种情况。在@Qualifier上面的示例中,我们也可以放弃@Repository注释,并@Bean在带有注释的类中使用注释,@Configuration以指示Spring在需要HibernateFooDao 时 使用 FooDao :

public interface FooDao {
    public List<Foo> findAll();
}

public class HibernateFooDao implements FooDao {

    @Override
    public List<Foo> findAll() {
        // ... find all using Hibernate ...
    }
}

public class SqlFooDao implements FooDao {

    @Override
    public List<Foo> findAll() {
        // ... find all using SQL ...
    }
}

@Configuration
public class FooConfiguration {

    @Bean
    public FooDao fooDao() {
        return new HibernateFooDao(); 
    }
}

使用此配置,Spring现在将具有在请求aHibernateDooDao 时 实例化a所需的逻辑 FooDao 。本质上,我们创建了一个Factory方法,该框架可用于实例化FooDao 何时需要的实例 。如果@Autowired在创建bean时排除了某个参数,我们可以通过在带有注释的方法中添加一个参数来反对这种依赖性@Bean。如果我们用@Component (或任何特殊的)注释组件,@Component Spring会在创建组件时知道注入依赖关系,但是由于我们是在Spring Framework外部直接调用构造函数,因此必须提供依赖关系。例如:

@Component
public class Bar {}

public class FooComponent {

    private final Bar bar;

    @Autowired
    public FooComponent(Bar bar) {
        this.bar = bar;
    }
}

@Configuration
public class FooConfiguration {

    @Bean
    public FooComponent fooComponent(Bar bar) {
        return new FooComponent(bar);
    }
}

Spring寻找满足fooComponent 方法参数的已注册候选对象,当找到一个候选 方法时,它将被传入并最终传递给 FooComponent 构造函数。请注意,任何带有注解@Component (或带有任何专业名称)的bean或使用其他@Bean方法创建的bean都可以注入到@Bean方法参数中。例如:

public class Bar {}

public class FooComponent {

    private final Bar bar;

    @Autowired
    public FooComponent(Bar bar) {
        this.bar = bar;
    }
}

@Configuration
public class FooConfiguration {

    @Bean
    public Bar bar() {
        return new Bar();
    }

    @Bean
    public FooComponent fooComponent(Bar bar) {
        return new FooComponent(bar);
    }
}

请注意,约定使用与@Beanbean相同的方法来命名方法,首字母小写。例如,如果我们要创建一个FooComponent,@Bean 通常会调用 用于创建Bean并用注释的方法fooComponent。

4. @RequestMapping

@Controller注释的大部分功能是从注释派生的,该@RequestMapping注释指示Spring创建一个映射到带注释方法的Web终结点。创建Web API时,框架需要知道如何处理对特定路径的请求。例如,如果对进行了HTTPGET调用https://localhost/foo,Spring需要知道如何处理该请求。此绑定(或映射)过程是@RequestMapping 注释的权限 ,它通知Spring应该将特定的HTTP动词和路径映射到特定的方法。例如,在上一节中,我们看到我们可以指示Spring使用以下代码段将HTTP映射GET到/foo

@Controller
public class FooController {

    @RequestMapping(value = "/foo", method = RequestMethod.GET)
    public List<Foo> findAll() {
        // ... return all foos in the application ... 
    }
}

请注意,可以将多个HTTP动词提供给method参数,但这在实践中是异常的。由于单个HTTP动词几乎总是提供给该方法的参数-这些动词通常最终会被GET,POST,PUT,和DELETE - Spring还包括可以被用来简化的创建四个附加的注释@RequestMapping的方法:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping

如果需要根路径(即与控制器路径匹配的路径),则不需要value参数。的@RequestMapping注释也可以应用于控制器本身,它设定了整个控制器的根路径。例如,以下控制器GET在/foo 路径上创建一个端点,在处 创建另一个POST端点/foo/bar:

@Controller
@RequestMapping("/foo")
public class FooController {

    @GetMapping
    public List<Foo> findAll() {
        // ... return all foos in the application ... 
    }

    @PostMapping("/bar")
    public void doSomething() {
        // ... do something ...
    }
}

@PathVariable

在某些情况下,可能会在路径中提供路径变量,这是正确处理请求所必需的。为了获得该路径变量的值,可以将参数提供给带有注释的方法,@RequestMapping并将@PathVariable注释应用于该参数。例如,如果需要实体DELETE的ID,则可以将ID作为路径变量提供,例如对的DELETE请求/foo/1。为了捕获 1 提供给负责处理DELETE请求的方法的代码,我们捕获了路径变量,方法是使用大括号将变量名称括起来,然后@PathVariable为处理程序方法的参数应用注释,其中将值提供给@PathVariable 与路径中捕获的变量名称匹配:

@Controller
public class FooController {

    @DeleteMapping("/foo/{id}")
    public void deleteById(@PathVariable("id") String id) {
        // ... delete Foo with ID "id" ... 
    }
}

默认情况下,@PathVariable假定的名称与带注释的参数的名称匹配,因此,如果参数的名称与路径中捕获的变量的名称完全匹配,则无需为@PathVariable注释提供任何值:

@Controller
public class FooController {

    @DeleteMapping("/foo/{id}")
    public void deleteById(@PathVariable String id) {
        // ... delete Foo with ID "id" ... 
    }
}

Spring将尝试将捕获的路径变量强制转换为以注释的参数的数据类型@PathVariable。例如,如果我们将ID path变量的值除为整数,则可以将id参数的数据类型更改为 int:

@Controller
public class FooController {

    @DeleteMapping("/foo/{id}")
    public void deleteById(@PathVariable int id) {
        // ... delete Foo with ID "id" ... 
    }
}

如果baz在路径(即/foo/baz)中提供了诸如字符串之类的值,则会发生错误。

@RequestParam

除了捕获路径变量之外,我们还可以使用注释捕获查询参数@RequestParam。用@RequestParam与@PathVariable注释相同的方式将参数装饰到处理程序方法,但是提供给@RequestParam注释的值与查询参数的键匹配。例如,如果我们希望GET对的路径进行HTTP调用/foo?limit=100,则可以创建以下控制器来捕获 limit 值:

@Controller
public class FooController {

    @GetMapping("/foo")
    public List<Foo> findAll(@QueryParam("limit") int limit) {
        // ... return all Foo objects up to supplied limit ... 
    }
}

与一样@PathVariable,@RequestParam可以省略提供给注释的值,并且默认情况下将使用参数的名称。同样,如果可能的话,Spring将把捕获的查询参数的值强制转换为参数的类型(在上述情况下为to int)。

@RequestBody

在请求主体是在调用中提供-commonly完成案件POST或PUT创造来电或更新条目- Spring提供了@RequestBody注解。与前两个注释一样,该@RequestBody注释将应用于处理程序方法的参数。然后,Spring会将提供的请求主体反序列化为参数的类型。例如,我们可以Foo 使用具有类似于以下内容的请求正文的HTTP调用创建一个新 的:

{"name": "some foo", "anotherAttribute": "bar"}

然后,我们可以创建一个包含与期望的请求主体匹配的字段的类,并创建一个捕获该请求主体的处理程序方法:

public class FooRequest {

    private String name;
    private String anotherAttribute;

    public void setName(String name) {
        this.name = name; 
    }

    public String getName() {
        return name;
    }

    public void setAnotherAttribute(String anotherAttribute) {
        this.anotherAttribute = anotherAttribute;
    }

    public String getAnotherAttribute() {
        return anotherAttribute;
    }
}

@Controller
public class FooController {

    @PostMapping("/foo")
    public void create(@RequestBody FooRequest request) {
        // ... create a new Foo object using the request body ...
    }
}

总结

尽管有许多Java框架,但Spring却是无处不在的,它是最普遍的一种。从REST API到DI,Spring包含丰富的功能集,这些功能使开发人员无需编写大量样板代码即可创建复杂的应用程序。Spring提供的一种机制是注释,它使开发人员可以修饰类和方法,并为它们提供上下文信息,Spring框架可以使用这些信息来代表我们创建组件和服务。由于Spring的普遍性,每个Java开发人员都可以从了解这些Spring注释以及它们在实践中的应用中受益匪浅。


原文链接:http://codingdict.com