分类
Spring MVC

Spring请求参数注解@RequestParam

译者注

原文

https://www.baeldung.com/spring-request-param

Demo

https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-mvc-basics-5

一、前言

在本文中,我们将要学习Spring的@RequestParam注解,和它的作用。
我们可以使用@RequestParam,从请求的参数或文件中,去解析查询参数。

二、简单的映射

假设我们定义了一个请求地址/api/foos,它接收了一个名为id的参数:

@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam String id) {
    return "ID: " + id;
}

在这个示例中,我们使用@RequestParam来解析id的查询参数。

一个简单的GET请求将会调用这个getFoos()方法:

http://localhost:8080/spring-mvc-basics/api/foos?id=abc
----
ID: abc

接下来,我们看一下这个注解的参数,包括:

  • 名称name
  • value
  • 必须required
  • 默认值defaultValue

3. 指定请求参数名称

在前面的例子中,变量名称和参数名称是相同的。但有时我们需要让这二者不同,或者我们没有使用SpringBoot,此时我们可能需要在编译时的配置文件中做一些额外的操作,否则参数名称将不会进行二进制编码。

幸运的是,我们可以使用name属性来配置请求参数@RequestParam的名称:

@PostMapping("/api/foos")
@ResponseBody
public String addFoo(@RequestParam(name = "id") String fooId, @RequestParam String name) { 
    return "ID: " + fooId + " Name: " + name;
}

我们还可以写成@RequestParam(value = “id”)或者干脆写成@RequestParam(“id”)

4. 可选的请求参数

使用@RequestParam注解的方法参数默认是必须的,这就一位置如果参数没有在请求中给出,就会发生错误:

GET /api/foos HTTP/1.1
-----
400 Bad Request
Required String parameter 'id' is not present

我们可以配置required = false来实现可选功能optional

@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam(required = false) String id) { 
    return "ID: " + id;
}

此时,无论请求中有没有给出这个参数,都可以正确的解析到我们刚才写的方法上:

http://localhost:8080/spring-mvc-basics/api/foos?id=abc
----
ID: abc

如果请求中没有给出参数,获取到的是null,而不是出错:

http://localhost:8080/spring-mvc-basics/api/foos
----
ID: null

4.1 使用Java8的Optional

我们还可以用Optional去装饰这个参数:

@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam Optional<String> id){
    return "ID: " + id.orElseGet(() -> "not provided");
}

此时,我们就不需要再去设置required属性了。如果请求中没有给出参数的值,就会返回默认值。

http://localhost:8080/spring-mvc-basics/api/foos
----
ID: not provided

5. 请求参数的默认值

我们通过defaultValue属性,为请求参数设置默认值:

@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam(defaultValue = "test") String id) {
    return "ID: " + id;
}

这样写的效果就类似required=false,用户不必再去提供这个参数:

http://localhost:8080/spring-mvc-basics/api/foos
----
ID: test

也可以提供参数:

http://localhost:8080/spring-mvc-basics/api/foos?id=abc
----
ID: abc

需要注意的是,当我们设置默认值属性时,必须属性required就已经设置为false了。

6. 映射所有的参数

我们还可以用映射,实现在没有定义参数名称的情况下,使用多个参数:

@PostMapping("/api/foos")
@ResponseBody
public String updateFoos(@RequestParam Map<String,String> allParams) {
    return "Parameters are " + allParams.entrySet();
}

上面的写法会返回请求的参数,如果发起请求就会:

curl -X POST -F 'name=abc' -F 'id=123' http://localhost:8080/spring-mvc-basics/api/foos
-----
Parameters are {[name=abc], [id=123]}

7. 映射多值参数

单一变量的请求参数可以包含多个值:

@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam List<String> id) {
    return "IDs are " + id;
}

并且SpringMVC会自动映射到一个逗号分隔的id参数:

http://localhost:8080/spring-mvc-basics/api/foos?id=1,2,3
----
IDs are [1,2,3]

或者一个分离的id列表:

http://localhost:8080/spring-mvc-basics/api/foos?id=1&id=2
----
IDs are [1,2]

8. 结论

在本文中,我们学习了如何使用@RequestParam注解。
你可以在Github上找到本文中示例的完整代码。

分类
Spring MVC

Spring 路径变量注解 @Pathvariable

译者注

原文

https://www.baeldung.com/spring-pathvariable

Demo

https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-mvc-java-2

一、前言

本文中我们将会学习Spring的路径变量注解@PathVariable
路径注解可以用来处理URI映射中的模板变量,并且设置这些变量作为方法参数。

我们来看看@PathVariable和它的各种属性。

二、简单的映射

@PathVariable注解的简单用法,可以通过主键来区分实体:

@GetMapping("/api/employees/{id}")
@ResponseBody
public String getEmployeesById(@PathVariable String id) {
    return "ID: " + id;
}

在这个例子中,我们使用@PathVariable注解来解析URI,前面的部分是固定的,后面是变量{id}

当我们是用GET方式向/api/employees/{id}发起请求时,就会给getEmployeesById方法提供参数id的值:

http://localhost:8080/api/employees/111
---- 
ID: 111

现在我们进一步了解这个注解,并且看一看它的属性。

3. 指定路径变量名称

在前面的例子中,我们跳过了“定义模板路径变量名称”的步骤,因为方法中的参数名称和路径变量的名称是相同的,Spring自动完成了匹配。

然而,如果路径变量名称和参数名称不同,我们可以在路径变量注解@PathVariable中指定它:

@GetMapping("/api/employeeswithvariable/{id}")
@ResponseBody
public String getEmployeesByIdWithVariableName(@PathVariable("id") String employeeId) {
    return "ID: " + employeeId;
}

发起请求时识别的变量如下:

http://localhost:8080/api/employeeswithvariable/1 
----
ID: 1

我们还可以像这样清楚的定义路径变量@PathVariable(value=”id”),而不是PathVariable(“id”)

4. 在单次请求中定义多个路径变量

根据实际使用情况,我们可以在一个控制器方法的URI请求中,使用一个以上的路径变量,当然,这个方法的参数也不止一个:

@GetMapping("/api/employees/{id}/{name}")
@ResponseBody
public String getEmployeesByIdAndName(@PathVariable String id, @PathVariable String name) {
    return "ID: " + id + ", name: " + name;
}

当发起请求时返回的结果如下:

http://localhost:8080/api/employees/1/bar
----
ID: 1, name: bar

我们也可以用一个java.util.Map<String, String>:类型的方法参数,处理一个以上的@PathVariable路径变量,如下面代码,id和name参数被打包成一个整体来处理:

@GetMapping("/api/employeeswithmapvariable/{id}/{name}")
@ResponseBody
public String getEmployeesByIdAndNameWithMapVariable(@PathVariable Map<String, String> pathVarsMap) {
    String id = pathVarsMap.get("id");
    String name = pathVarsMap.get("name");
    if (id != null && name != null) {
        return "ID: " + id + ", name: " + name;
    } else {
        return "Missing Parameters";
    }
}

请求的结果:

http://localhost:8080/api/employees/1/bar
----
ID: 1, name: bar

然而当路径变量@PathVariable中出现句点符号(.)时,就会出现一些小问题。
对于这种少数情况的讨论可以查看链接

5. 可选的路径变量

在Spring中,使用@PathVariable注解的方法参数默认是必要(required)的,即一旦使用注解就必须把值传过来:

@GetMapping(value = { "/api/employeeswithrequired", "/api/employeeswithrequired/{id}" })
@ResponseBody
public String getEmployeesByIdWithRequired(@PathVariable String id) {
    return "ID: " + id;
}

如上所示,这个控制器同时处理两个路径,/api/employeeswithrequired/api/employeeswithrequired/1 request。然而因为这个方法使用了@PathVariables注解,所以它不能处理发送到这个没有参数的/api/employeeswithrequired路径的请求:

http://localhost:8080/api/employeeswithrequired
----
{"timestamp":"2020-07-08T02:20:07.349+00:00","status":404,"error":"Not Found","message":"","path":"/api/employeeswithrequired"}

http://localhost:8080/api/employeeswithrequired/1
----
ID: 111

对于这种问题,有两种处理办法。

5.1 设置@PathVariable注解为非必要(required = false)

我们可以设置@PathVariable注解的必要(required)属性为false,来让它变成可选参数,同时,加入对于参数为空时的处理办法:

@GetMapping(value = { "/api/employeeswithrequiredfalse", "/api/employeeswithrequiredfalse/{id}" })
@ResponseBody
public String getEmployeesByIdWithRequiredFalse(@PathVariable(required = false) String id) {
    if (id != null) {
        return "ID: " + id;
    } else {
        return "ID missing";
    }
}

当对于这个API发起不带参数的请求时,结果如下:

http://localhost:8080/api/employeeswithrequiredfalse
----
ID missing

5.2 使用java.util.Optional

介绍了Spring4.1之后,在JAVA8以后的版本中,我们也可以使用java.util.Optional来处理非必要的路径参数:

@GetMapping(value = { "/api/employeeswithoptional", "/api/employeeswithoptional/{id}" })
@ResponseBody
public String getEmployeesByIdWithOptional(@PathVariable Optional<String> id) {
    if (id.isPresent()) {
        return "ID: " + id.get();
    } else {
        return "ID missing";
    }
}

现在,如果不在请求中指定路径变量id,我们将会得到默认的返回结果:

http://localhost:8080/api/employeeswithoptional
----
ID missing

5.3 使用Map<String, String>类型的方法参数

在前面的示例中,我们可以使用一个java.util.Map类型的方法参数去处理URI中的所有路径变量。现在,我们也可以这样去处理非必要路径变量的情况:

@GetMapping(value = { "/api/employeeswithmap/{id}", "/api/employeeswithmap" })
@ResponseBody
public String getEmployeesByIdWithMap(@PathVariable Map<String, String> pathVarsMap) {
    String id = pathVarsMap.get("id");
    if (id != null) {
        return "ID: " + id;
    } else {
        return "ID missing";
    }
}

6. @PathVariable的默认值

开箱即用,@PathVariable注解没有定义默认值的方法。然而,我们可以用上面提到的一些办法,来让默认值满足我们的需要,只需要检查路径变量是否为null。
例如,使用java.util.Optional<String, String>,我们可以验证路径变量是不是空值,如果它是空值,就可以返回一个默认值:

@GetMapping(value = { "/api/defaultemployeeswithoptional", "/api/defaultemployeeswithoptional/{id}" })
@ResponseBody
public String getDefaultEmployeesByIdWithOptional(@PathVariable Optional<String> id) {
    if (id.isPresent()) {
        return "ID: " + id.get();
    } else {
        return "ID: Default Employee";
    }
}

7. 结论

在本文中,我们讨论了如何使用Spring的路径变量注解@PathVariable
我们有很多高效的方法去应多不同的使用场景,例如“可选参数”和“返回默认值”等。

你可以在Github上获得本文中的示例代码

分类
spring

Spring @Primary 注解介绍

1. 概述

本文中我们将共同学习Spring自3.0版本引入的@Primary注解。

单词Primary意为首要的、主要的,其功能与名称相同:在依赖注入的过程中,当同一类型存在多个bean时,将首要(首先、优先)注入使用@Primary 注解的那个。

2. 适用场景

有些时候我们需要为同一类型注册多个不同的bean。

比如下述代码中我们为类型Employee(员工)提供了两个不同的bean:

@Configuration
public class Config {
 
    @Bean
    public Employee❶ JohnEmployee() {
        return new Employee("John")❷;
    }
 
    @Bean
    public Employee❶ TonyEmployee() {
        return new Employee("Tony")❷;
    }
}
  • ❶ 类型均为Employee
  • ❷ 返回两个不同的bean

然后使用@Autowired进行注入:

    @Autowired❶
    Employee❷ employee;
  • 此时当我们尝试 ❶注入 ❷Employee 时,则将发生NoUniqueBeanDefinitionException异常:
Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springPrimaryApplication': Unsatisfied dependency expressed through field 'employee'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'club.codedemo.springprimary.Employee' available: expected single matching bean but found 2: JohnEmployee,TonyEmployee

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

我们通常使用@Qualifier(bean的名称) 来指定具体注入哪个bean以规避此类异常。关于@Quailfer的更多用法,请参考本文

本文我们将重点介绍采用@Primary注解来处理此类问题。

3. @Primary@Bean 结合使用

在注入过程中由于多个bean满足某一类型,同时这些bean之间又没有主次之分,所以Spring无法擅自做主为我们选择注入其中的某一个bean。

@Primary 则会告之Spring哪个bean的地位是主要的、首要的、要优先考虑的,此时当发生类型情况下,Spring在注入时则会优先使用这个bean。

    @Bean
    @Primary ★
    public Employee JohnEmployee() {
        return new Employee("John");
    }

值得注意的是:同类型被声明为Primary的bean最多只能有一个。如果我们在同一类型的Bean上声明多个Primary,同样会发生NoUniqueBeanDefinitionException异常,错误代码示例如下:

    @Bean
    @Primary ✘
    public Employee JohnEmployee() {
        return new Employee("John");
    }

    @Bean
    @Primary ✘
    public Employee TonyEmployee() {
        return new Employee("Tony");
    }
  • 同一Employee类型,在多个Bean中使用了 @Primary注解,将引发NoUniqueBeanDefinitionException异常。

此时Spring在注入时发现了多个被声明为的bean,两个bean的优先级相同。Spring同样没有办法自作主张的注入一个其中一个bean给我们,报错信息如下:

more than one 'primary' bean found among candidates: [JohnEmployee, TonyEmployee]
多于一个主bean被找到...

4. @Primary@Component 结合使用

@Primary还可以与@Component注解结合使用。

@Component
@Primary
public class SmsServiceAliImpl implements SmsService {
    @Override
    public void sendMessage(String phone, String message) {
    }
}

@Component
public class SmsServiceBaiduImpl implements SmsService {
    @Override
    public void sendMessage(String phone, String message) {
    }
}

由于@Service、@Controller等注解也属于@Component注解,所以@Primary同样可以与它们结合使用。

@Service
@Primary
public class FooServiceImpl implements FooService {
}

5. 总结

当同一类型存在多个bean时,使用@Primary注解可以轻松的确定一个bean出来,这为Spring在完成注入时提供了依据。既然被称为bean,则必须保证其唯一,所以相同类型被@Primary声明的Bean最多只能有一个。

分类
spring

Spring @Qualifier 注解

1. 概述

本文我们将共同学习@Qualifier注解的作用以及其使用方法。在此基础上,将与@Primary 注解做一个简单的对比。

2. 自动装配的唯一性要求

在Spring项目中 @Autowired 注解是完成依赖注入的方式之一。但有些时候,此注解会在应该注入谁的问题上犯嘀咕。

在默认情况下,Spring是通过类型来完成依赖注入的。

注入过程中如果当前容器中出现多个bean的类型相同,Spring框架则会抛出 NoUniqueBeanDefinitionException 异常,来告诉我们:由于要注入类型的Bean在当前容器中不唯一,所以Spring无法为我们做主此时应该注入哪个。

比如:

@Component("fooFormatter★")
public class FooFormatter implements Formatter ❶ {
 
    public String format() {
        return "foo";
    }
}
 
@Component("barFormatter★")
public class BarFormatter implements Formatter ❶{
 
    public String format() {
        return "bar";
    }
}
 
@Component
public class FooService {
     
    @Autowired ❷
    private Formatter formatter;
}
  • ★ 自定义bean的名字,将在后面用到它
  • ❶ 两个bean的类型均为Formatter
  • ❷ 自动注入时将发生NoUniqueBeanDefinitionException异常。

❷这是由于此时满足类型为FooService的Bean有两个(fooFormatter以及barFormatter),而Spring并不能确认我们的真实意图是注入哪一个。

解决上述问题有多种方案,使用@Qualifier注解的方式是解决方案之一。

3. @Qualifier 注解

 @Qualifier 注解将显式的告之Spring依赖注入时应该注入的具体bean。

比如:

public class FooService {
     
    @Autowired
    @Qualifier("fooFormatter❶")
    private Formatter formatter;
}
  • ❶ 该值为前面在定义Bean时@Component注解中的值

@Qualifier显式地指名此时注入的bean为fooFormatter,所以即使barFormatter的类型同样为Formatter,此时也不会面临Spring不知道应该注入谁的问题。

我们注意到❶中使用了名称"fooFormatter",这是由于我们在前面定义Bean时在@Component注解中使用了相同的名称"fooFormatter"。在实现自定义Bean的名称功能时,除了使用@Component注解以外,还可以使用@Qualifier注解,两者的作用相同。

@Component
@Qualifier("fooFormatter") ❶
public class FooFormatter implements Formatter {
    //...
}
 
@Component
@Qualifier("barFormatter") ❶
public class BarFormatter implements Formatter {
    //...
}
  • ❶ 在@Qualifier注解中自定义Bean的名称

4. @Qualifier 与 @Primary

除了@Qualifier以外,还有一个@Primary注解也拥有在依赖注入时消除歧义的功能。

Primary译为主要的、首要的,该注解的作用是:当某个声明注入的类型存在多个bean时,除非你显式地声明使用哪个bean,否则将使用那个以@Primary注解的首要Bean。

示例如下:

@Configuration
public class Config {
 
    @Bean
    public Employee johnEmployee() {
        return new Employee("John");
    }
 
    @Bean
    @Primary★
    public Employee❶ tonyEmployee() {
        return new Employee("Tony");
    }
}
  • ❶ 两个Bean的类型相同,均为Employee
  • ★ 此Bean被声明为:首要的、主要的。

此时,若未声明注入的Bean,则使用方法tonyEmployee()的返回值生成的Bean来完成注入:

public class FooService {
     
    @Autowired ★
    private Employee employee;
}
  • ★ 此时将注入以@Primary注解的方法tonyEmployee()的返回值生成的Bean。

注意:Bean是唯一的,多次注入Employee,仅仅会执行一次tonyEmployee()方法。

前面我们有介绍@Primary时,多次强调了:未显式声明时才生效。也就是说一旦在注入时,显式地声明了注入的Bean名称则@Primary将失效。也就是说,在优先级上做比较:@Qualifier("xx") 高于 @Primary

比如:

@Component("fooFormatter")
@Primary ❶
public class FooFormatter implements Formatter {
 
    public String format() {
        return "foo";
    }
}
 
@Component("barFormatter")
public class BarFormatter implements Formatter {
 
    public String format() {
        return "bar";
    }
}

@Component
public class FooService {
     
    @Autowired
    @Qualifier("barFormatter") ★
    private Formatter formatter1;
}
  • ❶ 被声明为Primary
  • ★ 此时注入的为barFormatter

5.  @Qualifier与按名称自动装配

还有一种解决冲突的方法是在@Authwired注解的字段上,使用一个与Bean名称相同的字段名,这也是在未显式声明依赖bean的情况下Spring使用的默认策略。

比如:

public class FooService {
     
    @Autowired
    private Formatter barFormatter★;
}
  • ★ 字段名barFormatter与Bean名相同,此时将忽略@Primary注解,而注入的为barFormatter

但当显式的声明bean时,Spring则会以以显示声明的为准:

    @Autowired
    @Qualifier("barFormatter") ★
    private Formatter fooFormatter❶;
  • ❶ 由于显式声明的存在,所以字段名匹配原则将失败
  • ★ 将按显式声明的bean名称进行自动装配

6. 总结

在依赖注入过程中Spring默认使用按类型进行匹配的装配原则,当存在多个bean均满足条件时,spring将优先查找是显式声明的bean,如果未显示声明bean则将按字段名称进行查找,如果字段名称未查找成功则将继续查找使用了@Primary注解的bean,以上方法均未查找成功,则将抛出NoUniqueBeanDefinitionException异常。在装配bean的优先级上:@Qualifier > 按字段名称匹配 > @Primary

分类
spring-boot

Spring Boot项目应该怎样进行项目配置

1. 概述

Spring Boot提供了很多实用的功能。这其中就包括了我们可以为项目定义配置文件,并轻松的访问配置文件中定义的属性。在上文 中,我们对Spring 项目配置的几种方法进行了介绍。

本文我们一起来深入的学习@ConfigurationProperties注解的使用方法。

2. 依赖

和其它文章一样,本文依赖于Spring Boot,版本为2.3.4:

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.4.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

同时引入用于属性校验的hibernate-validator

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-validator</artifactId>
			<version>6.1.6.Final</version>
		</dependency>

Hibernate Validator做为java内置验证器的补充,能够处理更多的验证场景。可点击查看官方文档来提供了更多信息。

3. 简单配置

官方文档强烈建议我们将项目的一些配置项封装到POJO中,比如我们如下定义用于发送短信的配置:

@Configuration ➊
@ConfigurationProperties(prefix = "sms") ➋
public class SmsConfigProperties {
    /**
     * 短信接口地址
     */
    private String apiUrl;

    /**
     * 请求方法
     */
    private String requestMethod;

    /**
     * 端口
     */
    private int port;

    /**
     * 接收提醒消息的邮箱
     */
    private String email;

    // 请自行补充setter/getter方法
  • @Configuration将在应用启动时在Spring应用上下文中创建一个Bean。
  • @ConfigurationProperties(prefix = "sms")注解将配置文件中以sms的属性与SmsConfigProperties中的属性进行一一关联。

所以此时我们可以配置文件application.properties中加入以下配置:

# 短信基础配置信息
sms.api-url=sms.codedemo.club
sms.requestMethod=post
sms.port=8088
sms.email=panjie@yunzhi.club

然后便可以通过SmsConfigProperties实例的getter方法来获取相应的属性值了:

    @Autowired
    SmsConfigProperties smsConfigProperties;
    ...
    this.smsConfigProperties.getApiUrl();

值得注意的是由于Spring框架对配置进行赋值时调用的是标准的setter方法,所以我们必须在配置类中为每一个属性提供相应的setter方法。

除使用@Configuration注解将配置类标识为Spring Bean以外,还可以通过在启动类上加入@EnableConfigurationProperties注解的方法:

@ConfigurationProperties(prefix = "foo")
public class FooConfigProperties {
    private String bar;
@SpringBootApplication
@EnableConfigurationProperties(FooConfigProperties.class) ➊
public class ConfigurationPropertiesInSpringBootApplication {
  • ➊ 在应用启动时创建相关的Bean。此时Spring同样将自动读取配置文件application.properties中的相关配置前缀来构造相应的配置信息。

Spring在进行配置绑定时,可以非常聪明地获取到相应的属性值,比如我们在SmsConfigProperties中定义了apiUrl,则Spring可以成功绑定以下任意格式至该字段:

sms.apiUrl=sms.codedemo.club
sms.apiurl=sms.codedemo.club
sms.api_url=sms.codedemo.club
sms.api-url=sms.codedemo.club
sms.API_URL=sms.codedemo.club

3.1 Spring Boot版本兼容性

在部分Spring Boot版本中(比如Spring 2.2.0),Spring在启动时会自动扫描启动类所在包下所有以@ConfigurationProperties注解的类。在这种情况下,我们可以删除配置类上的@Component 相关注解(比如我们在上文中应用的 @Configuration),而只需要在配置类上保留@EnableConfigurationProperties即可:

@ConfigurationProperties(prefix = "scan.foo")
public class ScanFooConfigProperties {
    private String bar;

如果你恰恰使用是具有自动扫描功能的Spring Boot版本,但却想阻止Spring这么做,则可以使用 @ConfigurationPropertiesScan 注解来自定义扫描的包:

@ConfigurationPropertiesScan("club.codedemo.configurationpropertiesinspringboot.scan")
public class ConfigurationPropertiesInSpringBootApplication {

此时Spring Boot在启动时将仅扫描指定包中以@ConfigurationProperties为注解的类。当然了,即使我们使用的是其它版本,也可以这么做来实现扫描特定包中的配置类的目的。

4. 嵌套属性

有时候需要在配置文件中定义一些特殊类型的属性,比如:List、Maps或者其它java类型,我们将其统称为嵌套属性。

比如我们创建一个用于存储发送短信时第三方平台要求的用户名、密码等信息的Credentials认证信息类:

/**
 * 认证信息
 */
public class Credentials {
    private String id;
    /**
     * 密钥
     */
    private String secret;
    /**
     * 认证令牌
     */
    private String token;

然后将其与其它类型的属性一并加入到短信配置类中:

public class SmsConfigProperties {
...
    /**
     * 签名
     */
    private List<String> signs;

    /**
     * 附加请求头
     */
    private Map<String, String> additionalHeaders;

    /**
     * 认证信息
     */
    private Credentials credentials;

    // 请自行补充setter/getter方法

则在配置文件application.properties中我们可以如下定义:

# 签名信息
sms.signs[0]=codedemo
sms.signs[1]=yunzhi

# 获取使用以下签名信息,与上述效果等同
# sms.signs=codedemo,yunzhi

# 附加头信息
sms.additionalHeaders.secure=true
sms.additionalHeaders.senduser=panjie

# 认证信息
sms.credentials.id=yourId
sms.credentials.secret=yourSecret
sms.credentials.token=yourToken

5. 在使用@Bean注解的方法上同时使用@ConfigurationProperties注解

我们同样可以在@Bean注解的方法上添加 @ConfigurationProperties 注解。这往往适用于把一些属性绑定到第三方的组件时。

比如存在以下的简单类:

public class BeanMethod {
    private String foo;

则我们可以将@Bean@ConfigurationProperties 综合添加到相关方法上来构造一个BeanMethod实例:

@Configuration
public class ConfigProperties {
    @Bean
    @ConfigurationProperties(prefix = "bean.method")
    public BeanMethod beanMethodFoo() {
        return new BeanMethod();
    }
}

此时配置文件中定义的以bean.method打头的属性便会自动的关联到BeanMethod实例上。

bean.method.foo=beanMethodBar

6. 属性验证

@ConfigurationProperties 遵从了JSR-303格式的验证规范,我们可以通过在类上添加@Validated注解的方法来启用属性验证功能:

@Configuration
@Validated ➊
@ConfigurationProperties(prefix = "sms")
public class SmsConfigProperties {
  • ➊ 启动属性验证功能。在某些版本的Spring Boot中,可省略该注解。

此时,我们便可以在配置类的相关属性上加入必要的验证信息了,比如我们要求必须提供apiUrl字段的值:

    /**
     * 短信接口地址
     * 此字段不为空
     * 请修改配置文件中的sms.api-url的值为空来测试@NotBlank注解
     */
    @NotBlank
    private String apiUrl;

规定字段的长度:

    /**
     * 请求方法
     * 长度介于3-8位之间
     */
    @Length(max = 8, min = 3)
    private String requestMethod;

规定字段的大小:

    /**
     * 端口
     * 介于1-65535之间
     */
    @Min(1)
    @Max(65535)
    private int port;

用正则表达式处理复杂规范:

    /**
     * 接收提醒消息的邮箱
     * 使用正则表达式校验邮箱格式
     */
    @Pattern(regexp = "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,6}$")
    private String email;

Spring Boot在启动项目时,将对添加了验证器的字段进行验证,当任意字段不符合验证规范时将抛出IllegalStateException异常并终止应用。比如我们将配置文件中sms.requestMethod的值设置为长度为2的字符串,则应用启动时将报如下错误信息:

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'sms' to club.codedemo.configurationpropertiesinspringboot.SmsConfigProperties$$EnhancerBySpringCGLIB$$6681a0ab failed:

    Property: sms.requestMethod
    Value: po
    Origin: class path resource [application.properties]:3:19
    Reason: length must be between 3 and 8


Action:

Update your application's configuration

在应用启动时发生异常并中断应用虽然对单元测试不够友好(水平问题,尚未掌握Spring Boot项目启动失败的测试方法),但对保证项目的健壮性却是非常非常实用的。

我们在上述代码了使用了部分来自于Hibernate的验证器,此部分验证器工作时依赖于Java Bean的getter和setter方法,所以在使用时应注意为每个字段均提供getter/setter方法。

7. 属性转换

Spring Boot提供的@ConfigurationProperties 支持多种属性转换。

7.1 Duration

比如我们在配置文件中定义两个类型为Duration的字段:

@Configuration
@ConfigurationProperties(prefix = "conversion")
public class ConversionProperties {
    /**
     * 默认的时间单位为ms
     */
    private Duration timeInDefaultUnit;
    /**
     * 在赋值时传入其它单位后缀,比如:ns
     */
    private Duration timeInNano;

然后在配置文件中如下定义:

# 时间单位自动转换
conversion.timeInDefaultUnit=30
conversion.timeInNano=50ns

则最终在配置实例中得到的timeInDefaultUnit的值为30ms,timeInNano的值为50ns。

Spring支持的时间单位有:ns、us、ms、s、m、h、d分别代表:纳秒、微秒、毫秒、秒、分钟、小时、天

默认的时间单位为毫秒ms,这也意味着当我们指定的时间单位为ms时,可以省略ms后缀。

我们还可以通过加入 @DurationUnit 注解来变更字段的默认时间单位,比如我们将某个字段的时间单位指定为天:

    /**
     * 指定默认时间单位为天
     */
    @DurationUnit(ChronoUnit.DAYS)
    private Duration timeInDays;

则如下配置后,timeInDays属性的值为10天:

conversion.timeInDays=10

7.2 DataSize

@ConfigurationProperties 还支持DataSize类型的自动转换,转换与设置的方法与Duration完全相同,比如我们在配置类中添加如下几个DataSize类型字段:

@Configuration
@ConfigurationProperties(prefix = "conversion")
public class ConversionProperties {

    ...

    /**
     * 默认数据单元为 byte
     */
    private DataSize sizeInDefaultUnit;

    /**
     * 支持传入带后缀的数据单位,比如GB
     */
    private DataSize sizeInGB;

    /**
     * 自定义数据单位为TB
     */
    @DataSizeUnit(DataUnit.TERABYTES)
    private DataSize sizeInTB;

然后加入如下配置:

# 数据大小单位自动转换
conversion.sizeInDefaultUnit=30
conversion.sizeInGB=50GB
conversion.sizeInTB=10

则配置实例中sizeInDefaultUnit的值为30字节,sizeInGB的值为50G,sizeInTB的值为10TB。

Spring支持的数据单位有:B、KB、MB、GB以及TB,同样的可以使用@DataSizeUnit来指定某个字段的默认单位。

7.3 自定义转换器

此外,Spring还支持自定义转换器。比如我们有如下员工Employee类:

public class Employee {
    private String name;
    /**
     * 薪水
     */
    private double salary;
    // 此处省略了setter/getter

同时在配置文件中加入以下配置信息:

conversion.employee=john,2000

则预实现将配置文件中的配置信息映射到Employee类中的目标,可以定义如下转换器:

@Component ➊
@ConfigurationPropertiesBinding ➋
public class EmployeeConverter implements Converter<String, Employee>➌ {

    @Override
    public Employee convert(String s) {
        String[] data = s.split(",");
        Employee employee = new Employee();
        employee.setName(data[0]);
        employee.setSalary(Double.parseDouble(data[1]));
        return employee;
    }
}
  • ➊ 声明为组件,以便Spring在启动时扫描到
  • ➋ 使用@ConfigurationPropertiesBinding注解标识该类为自定义转换类
  • ➌ 自定义的转换类需要实现Converter接口

此时当我们在配置文件中使用Employee类时,则会自动调用上述自定义转换器以达到数据转换的目的:

@Configuration
@ConfigurationProperties(prefix = "conversion")
public class ConversionProperties {
    ...
    private Employee employee;
    ...

8. @ConfigurationProperties绑定不可变类

自Spring 2.2开始,我们可以使用@ConstructorBinding 注解来绑定配置文件。该方法用于绑定不可变Immutable配置:

// @Configuration ➍
@ConfigurationProperties(prefix = "sms.credentials")
@ConstructorBinding ➌
public class ImmutableCredentialsProperties {
    /**
     * 字段被声明为final,初始化后不可变
     */
    private final➊ String id;
    /**
     * 字段被声明为final,初始化后不可变
     */
    private final String secret;
    /**
     * 字段被声明为final,初始化后不可变
     */
    private final String token;

    public ImmutableCredentialsProperties➋(String id, String secret, String token) {
        this.id = id;
        this.secret = secret;
        this.token = token;
    }

    public String getId() {
        return id;
    }

    public String getSecret() {
        return secret;
    }

    public String getToken() {
        return token;
    }

    // 注意,此类无setter方法 ➎
  • ➊ 字段类型为final
  • ➋ 于构造函数中对属性赋初值
  • 标明此实例在初始化不可改变
  • 一定不能有@Configuration注解!
  • 一定不能有setter方法!

最后我们需要注意的是:由于➍不能加@Configuration注解,所以若想使该配置类生效,则需要在系统启动类上加入@EnableConfigurationProperties@ConfigurationPropertiesScan注解。

@EnableConfigurationProperties({FooConfigProperties.class, ImmutableCredentialsProperties.class★})
@ConfigurationPropertiesScan("club.codedemo.configurationpropertiesinspringboot.scan")
public class ConfigurationPropertiesInSpringBootApplication {

9. 总结

本文中,我们介绍了Spring Boot项目中进行配置的几种方法。在Spring Boot中我们可以通过加入前缀的方式来快速的将配置信息绑定到配置类中。即可以绑定基本属性,又可以绑定特殊类型,在绑定的过程中还可以加入相应的验证器。支持对特殊类型的绑定,也支持自定义的转换器。此外,我们还提供了带有相对完整测试用例的demo以期降低本文的学习难度。

分类
spring

Spring Bean 注解

1. 概述

本文中我们将讨论Spring中用于定义Bean的几种注解。

在Spring中有多种定义Bean的方式:比如可以使用xml文件来配置bean、在配置类中使用@Bean注解来标识某个方法,或者使用来自于 org.springframework.stereotype 包中的注解来标识当前应用包中的某个类。

2. 扫描组件

扫描组件功能被开启的情况下,Spring能够自动的扫描特定包(包含子包)下的定义的所有bean。

在@Configuration的类上同时使用@ComponentScan注解可以自定义Spring在启动时扫描的包

@ComponentScan(basePackages = "club.codedemo.outpackage")

如上代码上,basePackages属性指明了要扫描的包的名称为club.codedemo.outpackage,则Spring应用在启动时会自动扫描该包下所有的可用Bean。

其实正是由于Spring Boot应用中的@SpringBootApplication包含了@ComponentScan注解,所以Spring Boot项目在启动时会扫描项目启动类所在包以及子包中的所有可用的Bean。

在Spring Boot项目中,往往使用@ComponentScan注解来定义扫描当前Spring Boot项目以外的包

同样的道理,如果我们想在项目启动时扫描某个类(该类必须使用@Configuration标识)中的Bean,则可以使用basePackageClasses属性:

@ComponentScan(basePackageClasses = OutClass.class)

通过观察属性名(basePackages,basePackageClasses)可以猜想出,该属性对应的值是可以接收数组类型的。当有多个包或是类需要被定义时可以如下使用:

@ComponentScan(basePackages = {"club.codedemo.outpackage", "xxx.xxx.xxx"})
@ComponentScan(basePackageClasses = {OutClass.class, Xxxx.class})

如果我们未给@ComponentScan注解传入任何参数则表示:扫描当前文件所在包以及子包下的所有Bean。

自java8开发,允许我们在同一个类上重复使用某一注解,比如我们可以重复使用@ComponentScan注解来标识ComponentScanConfig类:

@ComponentScan(basePackages = "club.codedemo.outpackage")
@ComponentScan(basePackageClasses = OutClass.class)
public class ComponentScanConfig {

如果你不喜欢这种方式,还可以使用@ComponentScans来合并多个@ComponentScan

@ComponentScans({
        @ComponentScan(basePackages = "club.codedemo.outpackage"),
        @ComponentScan(basePackageClasses = OutClass.class)
})
public class ComponentScanConfig {

当使用xml配置时,代码也很简单:

<context:component-scan base-package="club.codedemo.outpackage" />

注意:受限于笔者水平,使用xml文件进行配置的方法并未在示例代码中体现。

3. @Component 注解

@Component注解作用于类中。Spring在进行Bean扫描时,能够检测到使用@Component注解的类。

比如:

@Component
public class Student {
}

默认情况下Spring在实例化一个Student作为Bean放置到自己管理的容器中,并且使用Student的首字小写(student)来作用bean的名称。如果你想自定义该bean的名称,则可以设置@Component注解的value属性:

@Component(value = "student")
public class Student {
}

而由于@Repository@Service@Configuration 以及 @Controller均是@Component的元注解,所以上述注解拥有@Component的特性及"待遇"。Spring应用在进行组件扫描时,也将扫描上述注解并按相同的命名规则来命名相应的bean。

4. @Repostiory 注解

在Spring应用中,一般使用DAO或是数据仓库Repository来充当数据访问层,进而完成与数据库的交互功能。我们往往使用@Repository注解来标识属于该层的类:

@Repository
public class StudentRepository {

@Repository注解可以自动对其内部发生的异常进行转换。比如当我们使用Hibernate作用JPA的实现时, Hibernate在数据操作中发生的异常被自动被捕获并转换为Spring中的DataAccessException异常子类被抛出。这种方式使我们能够使用统一的方式来处理数据访问层的异常。

预开启上述异常转换,则还需要声明一个PersistenceExceptionTranslationPostProcessor bean:

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }

XML配置如下:

<bean class=
  "org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

5. @Service 注解

一般情况下,我们会使用@Service来标识一个负责处理业务逻辑的service layer服务层。

@Service
public class CourseServiceImpl implements CourseService {
}

6. @Controller 注解

@Controller用于标识Spring MVC中的控制器:

@Controller
public class StudentController {
}

7. @Configuration 注解

当某个类想在方法中中使用@Bean注解定义bean时,则需要在该类上启用@Configuration注解

@Configuration
public class ComponentScanConfig {
 
    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }
}

8. Stereotype固有注解以及AOP

当我们使用Spring的固有注解时,我们可以非常轻松地创建一个针对Spring固有注解的切点。

比如在实际的生产项目中我们需要获取用户在使用过程中的慢查询。则可以配合@AspectJ注解如下实现:

@Aspect
@Component
public class PerformanceAspect {

    @Pointcut("within(@org.springframework.stereotype.Repository *)")
    public void repositoryClassMethods() {}

    @Around("repositoryClassMethods()")
    public Object measureMethodExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取切点方法执行的起始、结束时间
        long start = System.nanoTime();
        Object returnValue = joinPoint.proceed();
        long end = System.nanoTime();

        // 获取切点方法的类名、方法名并打印执行时间
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Long costTime = TimeUnit.NANOSECONDS.toMillis(end - start);
        System.out.println("执行:" + className + "->" + methodName +
                "耗时:" + costTime  + "ms");

        if (costTime > 1000) {
            // 耗时大于1秒的认为是慢查询,根据实际情况进行后续操作
            // 比如可以实时的短信预警、钉钉预警、邮件预警、推送到日志服务器等
        }

        return returnValue;
    }
}

上述代码中我们创建了一个切点,该切点的作用范围是:以@Repository为注解的类下的所有方法。然后使用了@Around 注解来关联该切点并拦截对应的方法、记录方法执行的时间等。

如上述代码注释所示,当执行花费的时间大于设定的上限时,我们则可以根据实现的需求发送相应的预警信息。

9. 总结

本文介绍了定义Bean的几种注解。同时介绍了自定义扫描包、类的方法。

在文章的最后以@Repository为例,定义了AOP切面并完成了获取慢查询的方法。通过该方法不难看出:数据访问层可以完全的专注了数据操作,而AOP切面则可以完全关注于查询时间。这或许就是程序开发时关注点分离的具体体现吧。

分类
spring spring-boot

Spring Boot注解

1、概述

Spring Boot自动配置的特性使得Spring在配置上很简约。本文中,我们将围绕Spring中两个核心包org.springframework.boot.autoconfigure以及org.springframework.boot.autoconfigure.condition包中的注解展开介绍。

2、@SpringBootApplication

@SpringBootApplication是Spring Boot项目接触到的第一个注解,用与标识应用的启动主类。

@SpringBootApplication
class SpringBootAnnotations {
 
    public static void main(String[] args) {
        SpringApplication.run(SpringBootAnnotations.class, args);
    }
}

从根本上讲@SpringBootApplication其实是@Configuration,@EnableAutoConfiguration和@ComponentScan三个注解的缩写,也可以认为@SpringBootApplication封装了上述三个注解。稍有些不同的是,@SpringBootApplication在封装@ComponentScan注解时,加入了一些默认的属性:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
		excludeFilters = {@ComponentScan.Filter(
				type = FilterType.CUSTOM,
				classes = {TypeExcludeFilter.class}
		), @ComponentScan.Filter(
				type = FilterType.CUSTOM,
				classes = {AutoConfigurationExcludeFilter.class}
		)}
)

也就是说在 Spring Boot项目中,完全可以使用上述注解代码段来代替@SpringBootApplication。当然了,实际的项目中肯定没有人这么做,试想谁又会舍近求远放着方便的不用,非要把简单的事情复杂化呢。

下面,让我们深入的了解一下Spring Boot的核心注解。

3、@EnableAutoConfiguration

@EnableAutoConfiguration的英文原意是:启用自动配置。该注解使得Spring Boot可以根据环境来自动检测并对项目进行一些自动化设置。比如当我们在pom.xml中同时引入Spring Data JPA以及H2数据库时,@EnableAutoConfiguration则会将Spring Data JPA的数据源自动的配置为H2。

需要注意的是该注解必须与@Configuration一起使用:

@Configuration
@EnableAutoConfiguration
class SpringBootAnnotationsConfig {}

4、条件注入(配置)

通常情况下,我们需要针对不同的环境(条件)来启用不同的配置,这一般被称为条件注入,可以借助本节中的条件注解来实现。

我们可以在使用@Configuration注解的类或使用@Bean注解的方法上放置条件注解,从而达到在特定的情况下使用特定的类或特定的方法的目的。在此,本文仅对其基本的使用方法进行介绍,预了解更多有关于本方面的知识,请访问此文

4.1. @ConditionalOnClass 以及@ConditionalOnMissingClass注解

@ConditionalOnClass表示:当某些类存在时,启用当前的配置;相反@ConditionalOnMissingClass表示:当某些类不存在时,启用当前配置。

@Configuration
@ConditionalOnClass(DataSource.class)
class MySQLAutoconfiguration {
    public MySQLAutoconfiguration() {
        System.out.println("DataSource.class存在")}
}

以下代码实现了:当DataSource.class存在时,Spring将使用该bean。

@Configuration
@ConditionalOnMissingClass("club.codedemo.springbootannotations.DataSource")
public class MySQLAutoconfiguration {
    public MySQLAutoconfiguration() {
        System.out.println("DataSource.class不存在“); }
}

以上代码实现了:当DataSource.class不存在时,Spring将使用该bean

4.2. @ConditionalOnBean以及@ConditionalOnMissingBean注解

除了可以基于类存在与否进行条件注入以外,还可以根据Bean是否存在来进行条件注入:

@Bean
@ConditionalOnBean(name = "dataSource")
LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    // ...
}

以上代码实现了:当名称为dataSource的bean存在时,注入本方法返回的Bean。

@Bean
@ConditionalOnMissingBean(name = "dataSource")
LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    // ...
}

以上代码实现了:当名称为dataSource的bean不存在时,注入本方法返回的Bean。

具体的验证代码请参阅本文提供的code demo。

4.3. @ConditionalOnProperty

使用@ConditionalOnProperty可以实现依据项目的属性值来进行注入。

@Bean
@ConditionalOnProperty(
    name = "usemysql", 
    havingValue = "local"
)
DataSource dataSource() {
    // ...
}

以上代码实现了:只有在项目的配置信息满足usemysql值为local时,注入该Bean。

4.4. @ConditionalOnResource

除根据项目的属性进行条件注入外,还可以根据项目资源文件夹中是否存在某配置文件来进行注入:

@Bean
//资源文件夹中存在mysql.properties文件时,该Bean生效
@ConditionalOnResource(resources = "classpath:mysql.properties")
Properties additionalProperties() {
    // ...
}
4.5.@ConditionalOnWebApplication以及@ConditionalOnNotWebApplication注解

@ConditionalOnWebApplication以及@ConditionalOnNotWebApplication注解可以基于当前应用程序是否为Web应用程序来进行配置。

@Bean
@ConditionalOnWebApplication
HealthCheckController healthCheckController() {
    // ...
}

如果当前应用属于Web应用,则上述Bean生效。

4.6. @ConditionalExpression

处理些稍复杂的注入需求,还可以使用@ConditionalExpression结合SpEL表达式来完成:

@Bean
@ConditionalOnExpression(${usemysql} &amp;&amp; ${mysqlserver == 'local'})
DataSource dataSource() {
    // ...
}

@ConditionalOnExpression注解接收的是SpEL表达式,当该表达式返回true时,该Bean生效;返回false,不生效。

4.7. @Conditional

对于更复杂的条件,还可以通过@Conditional结合创建自定义类的方式来实现:

@Conditional(CustomCondition.class)
Properties additionalProperties() {
    //...
}

5. 结论

本从由SpringBootApplication注解入手,对Spring Boot的条件配置进行了讲解。在实际的使用过程中,还需要根据项目的实际情况选择适合的技术。我们说适用的就是最好的,切不可在实际的项目为了实现技术而实现技术。

本文资源: