分类
DevOps spring-boot

SpringBoot应用作为服务运行

译者注

原文

https://www.baeldung.com/spring-boot-app-as-a-service

1、 前言

本文中,我们探讨把SpringBoot应用作为服务运行的一些方案。
首先,我们将会去解释web程序的打包选项和系统服务。
然后,当需要同时运行在linux和Windows系统时,我们有哪些办法来实现这个目的。
最后,我们将会总结一些引用文章,以便帮助读者获取更多信息。

2、项目设置和构建说明

2.1 打包

Web应用以传统方式被打包成web应用存档(Application aRchives,WAR),然后部署到一个web服务器。
SpringBoot应用既可以被打包成WAR也可以被打包成Jar文件,具体如何打包取决于你的应用服务是否需要安装配置的过程。

2.2 Maven配置

我们先从定义Maven的配置文件pox.xml开始:

<packaging>jar</packaging>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.0.RELEASE</version>
</parent>

<dependencies>
    ....
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <executable>true</executable>
            </configuration>
        </plugin>
    </plugins>
</build>

打包一定要设置为jar,在本文编写时,我们使用的是最新的SpringBoot稳定版,但实际上只要版本高于1.3就可以了。
你可以在这篇文章中找到可行的版本。

2.3 构建你的应用程序

在应用程序根目录执行这条命令来完成构建:

$ mvn clean package

然后,就可以在target目录中找到可执行的JAR文件。
如果要执行这个文件,需要在命令行中执行这条命令:

$ java -jar your-app.jar

此外,你仍然需要通过-jar选项来调用Java解释器。
有许多原因可以解释,为什么通过服务运行应用程序是一个好的办法。

3. 在linux中运行

为了作为一个后台进程运行这个程序,我们需要使用UNIX命令nohup,但出于某些原因,这并不是最好的办法。在这篇文章中给了很好的解释。
我们需要为我们的进程生成一个守护进程。在Linux下,我们需要选择:使用传统的System V初始化脚本还是使用Systemd配置文件,来配置守护进程。前者是被人们普遍认可的,但它逐渐被后者取代。

你可以在这篇文章中找到二者的详细区别。

为了加强安全性。我们先创建一个特殊用户,来运行这个服务,并且改变这个JAR文件的权限:

$ sudo useradd baeldung
$ sudo passwd baeldung
$ sudo chown baeldung:baeldung your-app.jar
$ sudo chmod 500 your-app.jar

3.1 System V init

SpringBoot可执行JAR文件使得服务启动进程十分简单:

$ sudo ln -s /path/to/your-app.jar /etc/init.d/your-app

上面的代码对你的可执行JAR文件创建了一个符号连接。必须使用到这个JAR文件的完整路径才能正确执行,否则符号连接无法正常工作。这个连接使得你可以让这个应用程序作为服务来启动:

$ sudo service your-app start

这个脚本支持标准服务的启动(start)、终止(stop)、重启(restart)、状态(status)命令,此外:

  • 它使用我们刚刚创建的“baeldung”用户来运行这个服务
  • 它在/var/run/your-app/your-app.pid文件中追踪这个程序的进程
  • 它写入控制命令到/var/log/your-app.log进程,以便我们可以查看程序的运行状态

3.2 systemd

systemd服务设置起来非常的简单。首先我们创建一个名为your-app.service的文件,并且填入下面的示例代码,然后把它放到/etc/systemd/system目录中:

[Unit]
Description=A Spring Boot application
After=syslog.target

[Service]
User=baeldung
ExecStart=/path/to/your-app.jar SuccessExitStatus=143 

[Install] 
WantedBy=multi-user.target

我们需要根据应用程序的实际情况来更新字段DescriptionUserExecStart。你需要能够执行前面提到的运行一个服务的基本命令。

3.3 Upstart

Upstart是一个基于时间的服务管理器,它是System V init的潜在替代者,它可以更好的控制不同守护进程的行为。

这篇安装教程十分有用,它对于几乎所有的Linux发行版都是可用的。
当使用Ubuntu时,你可能已经安装和配置好了,请检查/etc/init文件中是否以upstart开头的任务。

我们创建一个名为your-app.conf的任务,来启动我们的SpringBoot程序:

# Place in /home/{user}/.config/upstart

description "Some Spring Boot application"

respawn # attempt service restart if stops abruptly

exec java -jar /path/to/your-app.jar

现在运行start your-app,你的服务就可以启动了。

Upstart提供了许多任务配置选项,你可以在这个地址中找到大多数信息。

4. 在Windows中

本小节中,我们有几种不同的办法来使用Windows服务运行Java JAR包。

4.1 Windows服务装饰器(Windows Service Wrapper

由于Java服务装饰器使用的GPL协议和Jenkins使用的MIT协议在兼容上存在困难,人们开启了Windows服务装饰器项目,也称为“winsw”。

Winsw提供了安装、卸载、启动、停止服务的方法。
它可以在Windows平台运行任何类型的可执行服务,而不是像Java Service Wrapper只能运行Java的程序。

首先从这里下载二进制文件。然后编辑用来定义我们的Windows服务的配置文件:

<service>
    <id>MyApp</id>
    <name>MyApp</name>
    <description>This runs Spring Boot as a Service.</description>
    <env name="MYAPP_HOME" value="%BASE%"/>
    <executable>java</executable>
    <arguments>-Xmx256m -jar "%BASE%\MyApp.jar"</arguments>
    <logmode>rotate</logmode>
</service>

最后,把winsw.exe重命名为MyApp.exe,以便和配置文件MyApp.xml匹配。然后就可以安装服务:

$ MyApp.exe install

相似的,也可以卸载(stop)、启动(start)、停止(stop),等等。

4.2 Java服务装饰器

如果你不介意Java服务装饰器项目的GPL许可协议,有一个可替代的方式来配置你的JAR文件作为Windows服务。
Java服务装饰器也需要你去定制一个配置文件,来声明如何把你的程序作为服务在Windows上运行。

这篇文章详细阐述了在Windows下如何配置一个JAR可执行文件作为服务运行,因此无需再重复之前的内容。

5. 额外的引用

我们也可以使用Apache Commons Daemon
项目的 Procrun来让SpringBoot应用程序作为Windows服务启动。
Procrun是一个应用程序集合,它允许Windows用户把Java应用程序装饰成为一个Windows服务。这样的服务可以在机器启动时自动启动,并且即使没有用户登录也会持续运行。

这里可以找到关于SpringBoot程序在Unix下运行的更多信息。
这篇文章也介绍了如何编辑Redhat的Systemd系统单元文件

最后,这篇快速指南描述了如何为你的JAR文件创建一个Bash脚本,以便让它自己运行。

6.结论

服务是的我们可以更高效地管理我们的应用程序,正如我们看到的,把SpringBoot设置为一个服务的过程比以往更简单了。

需要注意的是,在用户权限方面,我们应该遵循重要而简单的安全措施,来运行你的服务。

分类
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类型转换

译者注

原文

https://www.baeldung.com/spring-type-conversions

Demo

https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-basic-customization-2

一、介绍

在这篇文章中,我们来学习Spring类型转换。
Spring为内建的类型提供了各种开箱即用的转换器,这意味着我们可以轻松的转换基本类型,例如String、Integer、Boolean或一些其他的类型。

除此之外,Spring也提供了一个固定的类型转换器SPI,以便我们用来自定义我们的转换器。

二、内建转换器

我们从Spring提供的开箱即用的转换器开始,先看看Spring内置的整数转换器:

@Autowired
ConversionService conversionService;

@Test
public void whenConvertStringToIntegerUsingDefaultConverter_thenSuccess() {
    assertThat(
      conversionService.convert("25", Integer.class)).isEqualTo(25);
}

我们需要做的就是:自动装入(autowire)Spring提供的转换器服务ConversionService,并且调用这个转换方法convert()。第一个参数是“我们想要转换的值”,第二个参数是“我们想要转换的目标类型”

此外,还可以从Spring转换为Integer,还有多种不同的组合可以使用。

3. 创建自定义转换器

我们看一个转换的例子,在这个例子中,我们把代表Employee对象的字符串转换为Employee实例对象。

先给出Employee类:

public class Employee {

    private long id;
    private double salary;

    // standard constructors, getters, setters
}

这个字符串的形式是“逗号分隔值”,分别表示idsalary(工资)属性,例如:"1,50000.00"

为了创建我们的自定义转换器,我们需要实现转换器接口Converter<S, T>,并且实现转换方法convert()

public class StringToEmployeeConverter
  implements Converter<String, Employee> {

    @Override
    public Employee convert(String from) {
        String[] data = from.split(",");
        return new Employee(
          Long.parseLong(data[0]), 
          Double.parseDouble(data[1]));
    }
}

现在还没有完成,我们还需要把刚刚创建的StringToEmployeeConverter类添加到格式注册器FormatterRegistry中,来告诉Spring我们用到了这个类。具体的做法就是实现WebMvcConfigurer类,并且重写Formatters()方法:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToEmployeeConverter());
    }
}

这样就完成了。我们的新转换器就在ConversionService中可用,并且我们可以使用同样的方式把它用在其他内置转换器上:

@Test
public void whenConvertStringToEmployee_thenSuccess() {
    Employee employee = conversionService
      .convert("1,50000.00", Employee.class);
    Employee actualEmployee = new Employee(1, 50000.00);
    assertThat(conversionService.convert("1,50000.00",
      Employee.class))
      .isEqualToComparingFieldByField(actualEmployee);
}

3.1 隐式转换

除了使用转换服务ConversionService来实现显式转换,Spring能在控制器方法中,使用所有已注册的转换器来实现隐式转换:

@RestController
public class StringToEmployeeConverterController {

    @GetMapping("/string-to-employee")
    public ResponseEntity<Object> getStringToEmployee(
      @RequestParam("employee") Employee employee) {
        return ResponseEntity.ok(employee);
    }
}

这样使用转换器更自然,我们对它进行一下测试:

@Test
public void getStringToEmployeeTest() throws Exception {
    mockMvc.perform(get("/string-to-employee?employee=1,2000"))
      .andDo(print())
      .andExpect(jsonPath("$.id", is(1)))
      .andExpect(jsonPath("$.salary", is(2000.0)))
}

你就可以看到,这个测试将会输出请求和相应的详细信息,就像下面这个JSON格式的Employee对象:

{"id":1,"salary":2000.0}

4. 创建一个转换器工厂

我们还可以创建一个用来按需创建转换器的转换器工厂ConverterFactory,这在创建枚举类型转换器的时候尤其有用。

我们来看一个简单的枚举:

public enum Modes {
    ALPHA, BETA;
}

接下来,创建一个“字符串到枚举类型转换器工厂”StringToEnumConverterFactory,这个工厂可以创建字符串到枚举的转换器:

@Component
public class StringToEnumConverterFactory 
  implements ConverterFactory<String, Enum> {

    private static class StringToEnumConverter<T extends Enum> 
      implements Converter<String, T> {

        private Class<T> enumType;

        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }

    @Override
    public <T extends Enum> Converter<String, T> getConverter(
      Class<T> targetType) {
        return new StringToEnumConverter(targetType);
    }
}

正如你看到的,这个工厂类内部使用一个转换器接口的实现。
需要注意的是,尽管我们将会使用我们的Modes Enum去演示用法,但我们在StringToEnumConverterFactory工厂的任何地方都没有提及这个枚举。我们这个工厂类会按需创建对于任何枚举类型的转换器。

下一步是注册这个工厂类,操作就像注册我们的Converter转换器一样,例子如下:

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new StringToEmployeeConverter());
    registry.addConverterFactory(new StringToEnumConverterFactory());
}

现在这个转换服务ConversionService可以把字符串转换成枚举了:

@Test
public void whenConvertStringToEnum_thenSuccess() {
    assertThat(conversionService.convert("ALPHA", Modes.class))
      .isEqualTo(Modes.ALPHA);
}

5. 创建一个通用转换器

通用转换器GenericConverter让我们在牺牲安全性的前提下,获得更大的灵活性,来创建一个更通用的转换器。
我们来设计一个可以转换Integer、Double、StringBigDecimal的转换器。
不需要去写三个转换器,而是使用通用转换器GenericConverter来达到这个目的。

第一步是告诉Spring我们希望这个转换器支持什么类型。操作时创建一个ConvertiblePair的集合(每个元素是一对类型,表示可以转换):

public class GenericBigDecimalConverter 
  implements GenericConverter {

@Override
public Set<ConvertiblePair> getConvertibleTypes () {

    ConvertiblePair[] pairs = new ConvertiblePair[] {
          new ConvertiblePair(Number.class, BigDecimal.class),
          new ConvertiblePair(String.class, BigDecimal.class)};
        return ImmutableSet.copyOf(pairs);
    }
}

下一步是在同样的方法里重写转换方法convert()

@Override
public Object convert (Object source, TypeDescriptor sourceType, 
  TypeDescriptor targetType) {

    if (sourceType.getType() == BigDecimal.class) {
        return source;
    }

    if(sourceType.getType() == String.class) {
        String number = (String) source;
        return new BigDecimal(number);
    } else {
        Number number = (Number) source;
        BigDecimal converted = new BigDecimal(number.doubleValue());
        return converted.setScale(2, BigDecimal.ROUND_HALF_EVEN);
    }
}

转换方法很简单,但TypeDescriptor在获取有关源和目标的详细信息时相当的灵活。
你可能已经猜到了,下一步是注册这个转换器:

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new StringToEmployeeConverter());
    registry.addConverterFactory(new StringToEnumConverterFactory());
    registry.addConverter(new GenericBigDecimalConverter());
}

调用这个转换器的方法和前面的例子相似:

@Test
public void whenConvertingToBigDecimalUsingGenericConverter_thenSuccess() {
    assertThat(conversionService
      .convert(Integer.valueOf(11), BigDecimal.class))
      .isEqualTo(BigDecimal.valueOf(11.00)
      .setScale(2, BigDecimal.ROUND_HALF_EVEN));
    assertThat(conversionService
      .convert(Double.valueOf(25.23), BigDecimal.class))
      .isEqualByComparingTo(BigDecimal.valueOf(Double.valueOf(25.23)));
    assertThat(conversionService.convert("2.32", BigDecimal.class))
      .isEqualTo(BigDecimal.valueOf(2.32));
}

6. 结论

本文中,我们学习了如何使用和拓展Spring的类型转换系统,并且给出了一些示例。
和往常一样,你可以在Github上找到本文的示例代码。

分类
spring

Spring @Autowired 注解

Autowired并不是一个英文词汇,而是auto + wired两个单词的组合。auto在英文中的原意为汽车,其本意是自动的意思,由于汽车可以不用人推、不用马拉自己跑,所以给它起了个auto的名字。auto源于automatic,即为自动的意思。比如汽车的自动变速器简称为AT,这其中的A则是指automatic。wired是wire的被动式,wire本意为电线。比如我们常说的无线wireless ,本意则是没有线的电线。wire加d,可理解为:串在一起的、绑在一起的、连在一起的。

所以Autowired的本意应该是:(将物体)自动连在一起。在计算机术语中,我们称其为:自动装配。

1. 概述

Spring 自2.5版本引用了注解驱动的依赖注入功能。该功能中最核心的注解即是@Autowired@Autowired 可以轻松的解决对象与对象间的依赖关系,将指定的bean自动的装配到其它需要它的bean中。

本文中,我们首先给出3种使用@Autowired 的方法,然后对使用@Qualifier 解决bean冲突做个简单的介绍。

2. 启用@Autowired 注解

Spring框架默认启用了依赖注入功能,所以在Spring中我们不需要做任何的配置即启用了@Autowired 注解的自动装配功能。

Spring就像一个大管家,管着很多个bean,哪里声明需要什么bean,它就负责提供什么bean。Spring Boot提供的@SpringBootApplication注解的作用之一便是:告知Spring大管家,应该到哪去获取到它管理的bean。

@SpringBootApplication
public class SpringAutowireApplication {

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

}

上述代码使得Spring在启动时扫描@SpringBootApplication 所在包以及其子包中的所有bean,并将其纳入Spring大管家的管理范围。在术语中,我们又称这个大管为:Spring Application Context(Spring应用上下文)。

3. 使用@Autowired

我们可以在类的属性、setter方法以及构造函数上使用@Autowired 注解。

3.1 在属性上使用@Autowired

@Autowired可以直接应用到类的属性上,即使该属性的类型不是public、同时也没有为其定义setter方法,也不会影响自动装配的功能。

为了演示自动装配功能,我们首先定义一个FooFormatter bean:

@Component
public class FooFormatter {
    public String format() {
        return "foo";
    }
}

接着便可以在属性上应用@Autowired来完成自动装配:

@Component
public class FooService {  
    @Autowired
    private FooFormatter fooFormatter;
}

此时,Spring将自动为FooService中的FooFormatter属性提供fooFormatter bean。我们也可以认为是自动(auto)将FooFormatterFooFormatter连接(wire)了起来,所以此项功能叫做Autowired。

3.2 在setter方法上使用@Autowired

还可以在setter方法上使用@Autowired,比如3.1中的代码还可改写为:

@Component
public class FooService {
    private FooFormatter fooFormatter;
    @Autowired
    public void setFooFormatter(FooFormatter fooFormatter) {
        this.fooFormatter = fooFormatter;
    }
}

与在属性上使用@Autowired来完成自动装配相比较,在setter上使用@Autowired没有什么明显的不 。当我们在装配某个属性的同时还希望执行某些逻辑操作时,往往会这么做。

3.3 在构造函数上使用@Autowired

最后,还可以在构造函数上使用@Autowired,这也是官方推荐的一种使用方式,比如3.2的代码还可以改写为:

@Component
public class FooService {
    private FooFormatter fooFormatter;
    @Autowired
    public FooService(FooFormatter fooFormatter) {
        this.fooFormatter = fooFormatter;
    }
}

值得注意的是,在较新的Spring版本中完全可以省略在构造函数上声明的@Autowired注解。也就是说以下代码同样可以完成自动装配功能:

@Component
public class FooService {
    private FooFormatter fooFormatter;

    public FooService(FooFormatter fooFormatter) {
        this.fooFormatter = fooFormatter;
    }
}

4. NoSuchBeanDefinitionException

当声明自动装配的bean不存在时:

public interface BarRepository★ {
}

@Service
public class BarService {
    @Autowired
    BarRepository barRepository;
}
  • ★ 只声明了接口,并对接口进行实现,所以并不存在类型为BarRepository的bean。

在进行单元测试时,Spring将抛出NoSuchBeanDefinitionException:

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'barService': Unsatisfied dependency expressed through field 'barRepository'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'club.codedemo.springautowire.section4.BarRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

NoSuchBeanDefinitionException直译为:没有找到相关的bean。根据实际情况不同,解决这个异常的方法分为两种:

第一种是前期我们容易犯的错误,即我们的确需要这样的一个bean,而且该bean也是由我们提供的,但忘记使用@Component 相关注解声明了。解决的方法当然是声明该类型的bean即可。

第二种是该bean可以有,也可以没有。那么此时则可以将@Autowiredrequired 属性设置为false :

@Service
public class BarService {
    @Autowired(required = false)
    BarRepository barRepository;
}

值得注意的是Spring Boot很友好的处理了NoSuchBeanDefinitionException异常,当发生NoSuchBeanDefinitionException异常时,Spring Boot给我们的提示信息如下:

***************************
APPLICATION FAILED TO START
***************************

Description:

Field barRepository in club.codedemo.springautowire.section4.BarService required a bean of type 'club.codedemo.springautowire.section4.BarRepository' that could not be found.

The injection point has the following annotations:
    - @org.springframework.beans.factory.annotation.Autowired(required=true)

大体是说:没有找到类型为BarRepository的bean。

5. NoUniqueBeanDefinitionException

默认情况下@Autowired注解是根据类型来完成自动装配的,在装配的过程中如果同一类型的bean存在多个,则会发生NoUniqueBeanDefinitionException异常。

此时便需要告知Spring大管家如何解决此类问题。

5.1 @Qualifier

qualifier译为限定符、限定语。

@Qualifier注解能够非常好的解决此类问题,比如我们当前有两个实现了Message 接口的bean:

public interface Message {
    String hello();
}

@Component
public class FooMessage implements Message {

    @Override
    public String hello() {
        return "foo";
    }
}

@Component
public class BarMessage implements Message {

    @Override
    public String hello() {
        return "bar";
    }
}

此时若直接使用@Autowired 注解尝试注入Message:

@Service
public class MessageService {
    @Autowired
    Message message;
}

由于FooMessageBarMessage都实现了Message接口,所以Spring容器中现在有两个同样是实现了Message接口的bean,此时若使用@Autowired 尝试注入Message, Spring便不知道应该是注入FooMessage 还是BarMessage,所以发生NoUniqueBeanDefinitionException异常。

NoUniqueBeanDefinitionException的字面意思也很好的说明了这个问题:定义的bean不唯一。

当同一类型存在多个bean时,可以使用@Qualifier 来显示指定bean的名称,从而解决了Spring按类型注入时不知道应该注入哪个bean的尴尬情况。

@Service
public class MessageService {
    @Autowired
    @Qualifier("fooMessage")
    Message message;
}

默认情况下Spring自动为每个bean添加一个唯一的名称,该名称的命名原则为:将类的名称由大驼峰变为小驼峰。所以FooMessage 的bean名为:fooMessage;BarMessage 的bean名为: barMessage。除此以外,我们还可以自定义bean的名称,比如将BarMessage 的bean名称定义为customBarMessage:

@Component("customBarMessage")
public class BarMessage implements Message {

@Qualifier 将根据声明的fooFormatter来完成对Formatter的注入。此时FooService 中被注入的FormatterFooFormatter 而非BarFormatter

5.2 自定义装配限定器

Spring还允许我们基于Qualifer 自定义用于限定注入特定Bean的注解。

@Qualifier
@Target({
        ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER
})
@Retention(RetentionPolicy.RUNTIME)
public @interface MessageType {
}

此时我们将注解应用到FooMessage 上:

@Component
@MessageType
public class FooMessage implements Message {

则可以结合@Autowired 完成限定注入FooMessage :

@Service
public class MessageService {
    @Autowired
    @MessageType
    Message message1;
}

由于自定义的@MessageType 继承了@Qualifier ,所以其也拥有@Qualifier 的特性。当多个相同类型的Bean均使用@MessageType 声明时,也可以使用@MessageType(bean的名称) 来指定自动装配的bean。

5.3 依据属性名称自动装配

Spring在自动装配过程中,当同一类型存在多个bean时,还会继续按属性名称来进行查找,比如:

    @Autowired
    Message message;

Spring首先查找类型为Message的bean,最终查找到两个:fooMessage以及barMessage;接着将会在找到的两个bean中继续按属性名称message进行查找;最终发现fooMessage以及barMessage均不匹配message,这时候就视为查找失败,抛出异常。

根据上述规律,当某类型存在多个bean时,也可以使用指定属性名称与bean名相匹配的方法来完成限定注入:

@Service
public class MessageService {    
    @Autowired
    Message barMessage;
}

此时将成功注入一个BarMessage的实例。

总结

@Autowired 自动装配注解是Spring的核心注解之一,我们可以使用@Autowired 按接口类型轻松的完成bean的注入,而不必关心具体的实现。是对面向接口编程具体的阐释。在实际的使用过程中,往往会遇到多个bean实现同一接口而产生冲突的情况,此时则需要配合其它方法来告知Spring大管家冲突的解决方案。本文主要对@Autowried 注解的使用方法以及如何解决冲突进行阐述。

分类
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-boot

Spring 项目配置的几种方法

1. 概述

本文中将讨论Spring项目中如何进行配置以及如何使用配置。文章中将分别展示两种方式:JAVA代码配置、XML配置。

Spring Boot项目完全继承了Spring项目的配置方式,不仅如此,其提供的一种新的配置方式使项目的配置更的高效。

2. 使用Java注解注释配置文件

自Spring 3.1开始,Spring提供了一个新的@PropertySource 用于向项目环境中添加配置源。

该注解在使用时需要@Configuration注解配合工作:

@Configuration
@PropertySource("classpath:sms.properties")
public class SmsProperties {

在上述代码的支撑下,我们便可以在应用中注入Enviroment,并通过调用getProperty()方法来轻松的获取到classpath:sms.properties文件中的配置项了:

    @Autowired
    Environment environment;

    public String getApiUrl() {
        return this.environment.getProperty("apiUrl");
    }

在注册配置源时,当我们需要设置某些数据源仅在某些条件下生效时,则可以使用${}占位符

@Configuration
@PropertySource("classpath:datasource-${spring.application.db-type:mysql}.properties")
public class DatasourceProperties {

classpath:datasource-${spring.application.db-type:mysql}.properties表示,如果spring.application.db-type配置项存在,则使用该配置项的值替代${spring.application.db-type:mysql},如果该配置项不存在,则使用mysql这个默认值来替换${spring.application.db-type:mysql}

假设spring.application.db-type存在且值为h2时,比如我们在application.properties中进行如下配置并提前导入该配置源:

spring.application.name=codedemo
spring.application.db-type=h2

则Spring会自动加载classpath:datasource-h2.properties配置文件。如果spring.application.db-type不存在,则会加载classpath:datasource-mysql.properties配置文件。

2.1 定义多配置源

自java8开始,我们可以在某个目标上重复使用同一个注解。所以当我们想定义多个配置源时,则可以如下使用:

@Configuration
@PropertySource("classpath:foo.properties")
@PropertySource("classpath:bar.properties")
public class FooBarProperties {

上述代码同时将classpath:foo.properties与classpath:bar.properties添加到了配置源中,此时我们便可以在Environment中获取上述两个配置文件定义的配置值了。

除此以外,如果你不习惯上述用法,或是你当前的JAVA版本并不支持在同一目标上定义多个相同注解,则可以使用@PropertySources注解,该注解可以接收数组类型:

@Configuration
@PropertySources({
        @PropertySource("classpath:foo.properties"),
        @PropertySource("classpath:bar.properties")
})
public class FooBarProperties {

值得注意的是:无论我们使用哪种方式定义多配置源,当配置源中的配置项发生冲突时,后面的配置源将覆盖前台的配置源。

3. 使用XML方式注册

使用XML方式配置时配置源时,需要将配置源添加到 <context:property-placeholder … > 元素中:

<context:property-placeholder location="classpath:foo.properties" />

此时Spring应用在启动时将加载/src/main/resources下的foo.properties配置文件。

如果在项目中配置使用 <property-placeholder>设置了多个配置源,那么请参考以下最佳实践:

  1. 应该为每个配置源均指定order属性,在Spring进行加载时,将按照指定的order属性顺序加载。
  2. 除最后一个配置源外(order最大的那个),其实的配置源都应该设置ignore-unresolvable=”true” 。

3.1 使用XML设置多数据源

在XML中,可以使用","来分隔多个配置源:

<context:property-placeholder location="classpath:foo.properties, classpath:bar.properties"/>

同样的,如果多个配置源定义了相同的配置,则只有最后定义的配置源中的配置生效。

4. 直接在项目使用配置值

除了可以注入Environment后调用getProperty方法来获取配置值以外,还可以使用@Value注解

    @Value("${key}")
    private String key;

注意:上述代码中使用@Value注入了key的值,此时若key存在于sms.properties中而非application.properties中。则必须保证sms.properties已被添加到配置源。

当然了,我们还可以为其提供一个默认值,以避免在未配置此项时发生异常:

    @Value("${token:defaultToken}")
    private String token;

此时当项目未定义token时,token值将被设置为defaultToken

也可以使用XML进行定义:

<bean id="dataSource">
    <property name="token" value="${token:defaultToken}" />
</bean>

更多详细的信息请参考:https://www.baeldung.com/properties-with-spring#usage

5. 配置Spring Boot

在Spring中配置完配置源后,需要通过EnvironmentgetProperty()方法来配置相关的配置项。Spring Boot则提供了一系列更简单、更直接的方法:

5.1 默认配置文件application.properties

Spring Boot在启动时会加载默认的配置的文件application.properties。这意味着:我们只需要在src/main/resources中提供application.properties文件,而不需要使用PropertySource等将其声明为配置源。

同时,当你想使用其它的文件作用项目默认配置文件时,仅仅需要在运行时加入如下参数即可。比如在项目启动时使用src/main/resources中的another-location.properties替换原application.properties文件,则可以使用以下命令启动:

java -jar app.jar --spring.config.location=classpath:/another-location.properties

自Spring Boot 2.3开始,该配置还加入了对正则表达式的支持,比如我们可以如下设置:

java -jar app.jar --spring.config.location=config/*/

此时Spring Boot将会查找所有满足上述正则表达式的配置文件。上述代码将在运行app.jar时,查找所有的当前app.jar所在文件夹/config目录下的配置文件。这种方式特别的适用于为某个应用配置多个配置文件的情景。

5.2 情景环境配置

注意:本文并未刻意的区域情景环境,在大多数文章中情景等于环境,只是说法不同罢了,本文也是如此。

在Spring Boot中可以轻松的为不同的环境定制指定的配置。

我们可以在src/main/resources文件夹中建立application-环境名称.properties文件,Spring应用在启动时将对按项目的当前情景名称选择加载对应的配置文件。

比如我们需要一个用于开发的"开发环境",将其名称起为dev。则需要在src/main/resources文件夹中建立application-dev.properties文件。此时,当项目的情景设置为dev时,此配置文件将被自动加载:

spring.profiles.active=dev

Spring能够非常聪明的处理dev环境:Spring先读取默认的application.properties中的配置,然后再读取当前情景dev对应的application-dev.properties配置。并使用application-dev.properties中定义的配置覆盖application.properties中的相同配置项。

5.3 单元测试中的配置

如果要为单元测试指定单独的配置,则可以在src/test/resources文件夹中创建相应的配置文件。与项目配置的规则相同,当单元测试的配置项发生冲突时,优先给高的配置文件会覆盖优先级低的配置文件。

5.4 @TestPropertySource 注解

我们还可以使用@TestPropertySource注解为单元测试来指定特定的配置文件:

@SpringBootTest
@TestPropertySource("classpath:test.properties")
public class TestWithProperties {

除了指定配置文件外,还可以直接在单元测试中定义属性:

@TestPropertySource(properties = "foo1=bar1")

@SpringBootTest注解也支持属性的定义:

@SpringBootTest(properties = {"foo2=bar2"})

5.5 @ConfigurationProperties 注解

相对于Spring,Spring Boot提供的@ConfigurationProperties注解能够快速的对配置文件按前缀进行分组,并自动将配置文件中的值映射到JAVA类的属性上。

比如我们有如下配置:

code-dome.domain=www.codedemo.club
code-dome.organization=mengyunzhi
code-dome.year=2020

则可以建立如下配置类

@Component
@ConfigurationProperties(prefix = "code-dome")
public class CodeDemoProperties {
    private String domain;
    private String organization;
    private int createYear;
    // 省略getter/setter
}

Spring Boot的@ConfigurationProperties注解的自动将配置中的属性映射到相应的配置类的字段上。而我们需要做的,仅仅是提供配置类的前缀。

如果你想了解更多关于Spring Boot配置映射的知识,还可以学习进阶文章

5.6 YAML文件格式

Spring同样支持YAML文件格式。在使用YAML做为配置的文件格式时,本文所讨论的所有效果基本上都生效。我们要做做仅仅是将.prpperties重命名为.yml,然后按照SnakeYAML规范来配置属性即可。

比如我们在application.properties中有如下配置:

spring.profiles.active=dev
spring.application.name=codedemo
spring.application.db-type=h2

code-dome.domain=www.codedemo.club
code-dome.organization=mengyunzhi
code-dome.create-year=2020

则使用YAML格式应该定义为:

spring:
  profiles:
    active: dev
  application:
    name: codedemo
    db-type: h2

code-dome:
  domain: www.codedemo.club
  organization: mengyunzhi
  create-year: 2020

同时YAML还支持在一个配置文件中定义多个情景模式:

---
# 使用---定义新的情景模式 mengyunzhi
spring:
  profiles: mengyunzhi

code-dome:
  domain: www.yunzhi.club
  organization: mengyunzhi

---
# 使用---定义新的情景模式 hebut
spring:
  profiles: hebut

code-dome:
  domain: www.hebut.edu.cn
  organization: hebut

值得注意的是:@PropertySource注解并不支持YAML格式,如果你使用了@PropertySource注解则必须提供一个.properties文件。

5.7 命令行模式

当我们使用java来启动打包后的项目文件时,可以使用如下命令来覆盖项目的相关配置项:

java -jar app.jar --property="value"

或者使用如下命令来变更系统属性,从而覆盖项目的相关配置项:

java -Dproperty.name="value" -jar app.jar

比如我们需要在运行项目时覆盖数据库的用户名,则可以使用:

java -jar app.jar --spring.datasource.username="newusername"

或者:

java -Dspring.datasource.username="newusername" -jar app.jar

5.8 环境变量

同样的,如果对某个运行JAVA应用的系统设置了相应的环境变量。Spring项目在被启动时,会自动的加载该环境变量,从而覆盖项目的相应的配置:

export code-dome.domain=www.codedemo.club
java -jar app.jar

此时app.jar应用读取到的code-dome.domain的值为www.codedemo.club

5.9 随机值

有时候你可能需要一些随机值,此时可以使用 RandomValuePropertySource,比如使用${random.int}来获取一个随机整型。

# 随机字符串
my.secret=${random.value}
#随机long
my.bignumber=${random.long}
#随机uuid
my.uuid=${random.uuid}
# 随机int
my.number=${random.int}
# 随机小于10
my.number.less.than.ten=${random.int(10)}
# 随机范围
my.number.in.range=${random.int[1024,65536]}

5.10 其它配置方法

除本文所描述的方法外,Spring Boot还支持多种配置方法。预了解更多的详情,请参考Spring Boot官方文档

6. 在Spring 3.0中手动配置

除了使用注解进行项目配置外,我们还可以使用代码进行相关配置。

需要注意的是:使用代码配置不是常规做法,除非你真的认为有必要,否则应该放弃这种又臭又长的配置方式。

比如添加配置文件java-manually.properties

@Bean
public static PropertyPlaceholderConfigurer properties() {
    PropertyPlaceholderConfigurer ppc
      = new PropertyPlaceholderConfigurer();
    Resource[] resources = new ClassPathResource[]
      { new ClassPathResource( "java-manually.properties" ) };
    ppc.setLocations( resources );
    ppc.setIgnoreUnresolvablePlaceholders( true );
    return ppc;
}

使用XML:

<bean 
  class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <list>
            <value>classpath:java-manually.properties</value>
        </list>
    </property>
    <property name="ignoreUnresolvablePlaceholders" value="true"/>
</bean>

7. 在Spring 3.1中手动配置

Spring 3.1启用了PropertySourcesPlaceholderConfigurer替代Spring 3.0中的PropertyPlaceholderConfigurer。在使用方法上大同小异:

    @Bean
    public static PropertySourcesPlaceholderConfigurer properties() {
        PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer
                = new PropertySourcesPlaceholderConfigurer();
        ClassPathResource[] resources = new ClassPathResource[] {
                new ClassPathResource("java-manually.properties")
        };
        propertySourcesPlaceholderConfigurer.setLocations(resources);
        propertySourcesPlaceholderConfigurer.setIgnoreUnresolvablePlaceholders(true);
        return propertySourcesPlaceholderConfigurer;
    }

需要注意的是,通过上述方法加载的配置项仅能够通过@Value来获取,若尝试使用Environment获取则将得到一个null值

8. 属性在上下文中的传递规则

属性在上下文中的传送规则是个老生常谈的问题。当我们的应用具有父上下文或子上下文时,父子上下文中可能会具有一些相同的核心功能。这不可避免的会带来一些冲突。那么我们应该如何避免这些冲突并使其朝着正确的方向发展呢?

下面我们给出一些简单的规则 :

8.1 如果配置文件是使用<proprty-placeholder>在XML中定义的

如果文件是在上下文中定义的:

  • @Value 在子上下文中
  • @Value 在父上下文中

如果文件是在上下文中定义的:

  • @Value 在子上下文中
  • @Value 在父上下文中

综上:<property-placeholder>在XML中定义的,仅在本上下文中有效。

最后,正如我们在前面提过的那样,使用<property-placeholder>定义的属性,并不会注入到Environment中,所以使用该方式定义的属性,在父子上下文中均无法通过environment.getProperty()获取。即:

  • environment.getProperty在子上下文中
  • environment.getProperty在父上下文中

8.2 如果属性是通过@PropertySource注解定义的

如果文件是在父上下文中定义的,则:

  • @Value 在子上下文中
  • @Value 在父上下文中
  • environment.getProperty 在父上下文中
  • environment.getProperty 在子上下文中

如果文件是在子上下文中定义的,则:

  • @Value 在子上下文中
  • @Value 在父上下文中
  • environment.getProperty在子上下文中
  • environment.getProperty在父上下文中

9 结论

项目离不开配置,能否快速、高效、准确的对项目进行配置很大程度上影响了项目开发、测试、运维的效率。本文从多方面对Spring、Spring Boot的项目配置进行了介绍,同时给出了多种获取项目配置的方法。在文章的最后对项目配置的上下文传递进行简单的总结。

分类
spring

Spring 情景设置

1. 概述

无论在任何项目中,项目配置都是避不开的核心章节:我们需要为同一项目的不同环境定制不同的配置。比如我们在开发时并不需要真正地向邮件服务器、短信服务器等发送数据,此时则需要一个开发环境的配置;比如我们在生产环境下应该启用正式的邮件服务、短信服务、日志推送服务等,此时则需要一个生产环境的配置;再比如在单元测试的环境中,我们更希望能够使用H2数据库来替换MYSQL等数据库,则此时还需要一个用于测试环境的配置。本文将围绕Spring项目的配置进行讨论。

在Spring配置的支持下,可以为不同的环境声明不同的Bean实现,进而达到满足不同环境功能要求的目的。下面,让我们来看下Spring项目是如何实现的。

2. 使用@Profile注解

Spring提供了一个非常简单的方式来指定特定环境下的Bean --- @Profile注解。

假设应用中的短信服务如下:

/**
 * 短信服务
 * @author panjie
 */
public interface SmsService {

    /**
     * 发送短信
     * @param phone 接收的手机号
     * @param context 发送的短信文本
     */
    void sentMessage(String phone, String context);
}

由于我们没有必要在开发环境中真实的发送短信,所以我们当前想实现:开发环境中在控制台中打印短信内容,而在生产环境中则调用实际的短信接口来发送短信:

@Service
@Profile("dev")
public class SmsServiceConsoleImpl implements SmsService {
    @Override
    public void sentMessage(String phone, String context) {
        // 在控制台中打印发送的手机号及内容
    }
}

上述代码中使用@Profile("dev")来标识SmsServiceConsoleImpl仅当项目情景为dev时生效。

@Service
@Profile("pro")
public class SmsServiceImpl implements SmsService {
    @Override
    public void sentMessage(String phone, String context) {
        // 调用短信api发送短信
    }
}

上述代码中使用@Profile("pro")来标识SmsServiceImpl仅当项目情景为pro时生效。

如果当前项目仅有两种模式或是想实现除了某种模式以外其它的模式全部生效,则还可以结合!来使用:

@Service
@Profile("!pro")
public class SmsServiceConsoleImpl implements SmsService {

上述代码实现了:当前环境不为pro时SmsServiceConsoleImpl生效。

3. 使用XML

同样还可以使用xml来进行配置:

<beans profile="dev">
    <bean id="smsServiceConsoleImpl" 
          class="club.codedemo.springprofiles.service.SmsServiceConsoleImpl" />
</beans>

4. 设置项目环境

Spring提供了多种设置项目环境的方法,本节中我们将一一展开:

4.1 实现WebApplicationInitializer接口

我们可以在Spring项目中,通过相应的编码来改变项目环境。需要注意的是:如果变更环境的代码发生在Spring读取环境以后,那么将不对Spring目的Bean配置产生影响。

方法一:调用ServletContext的setInitParameter方法。

@Configuration
public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        servletContext.setInitParameter( "spring.profiles.active", "dev");
    }

此方法将在原来的项目环境上进行追加,比如原来的项目环境为pro,则使用上述上码完成追加后,项目环境将变更为dev、pro。

4.2 注入ConfigurableEnvironment

也可以使用注入ConfigurableEnvironment的方法来设置项目环境:

    @Autowired
    private ConfigurableEnvironment env;
    ...
    env.setActiveProfiles("dev");

上述代码将覆盖项目的原情景,比如在执行上述代码前项目的情景为pro,则执行上述代码后项目情景将变更为dev。

4.3 设置web.xml

在web应用中,还可以通过设置web.xml来设置项目情景:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/app-config.xml</param-value>
</context-param>
<context-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>dev</param-value>
</context-param>

4.4 设置JVM

还可以通过设置JVM的系统参数来达到设置Spring项目情景的目的,比如我们在项目启动时加入如下JVM系统参数:

-Dspring.profiles.active=dev

4.5 环境变量

在Liunx/unix系统中,还可以通过设置环境变量的方法来设置项目情景:

export spring_profiles_active=dev

4.5 MAVEN配置

如果你的项目是一个标准的maven项目,那么还可以在maven项目中声明以下配置(pom.xml):

<profiles>
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <spring.profiles.active>dev</spring.profiles.active>
        </properties>
    </profile>
    <profile>
        <id>prod</id>
        <properties>
            <spring.profiles.active>prod</spring.profiles.active>
        </properties>
    </profile>
</profiles>

然后在项目的配置文件application.properties:中如下设置spring.profiles.active:

spring.profiles.active=@spring.profiles.active@

如上述代码所示,我们使用了@关键字@的方法来获取maven项目中profile的相关值。

接着启用资源过滤器:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
        </resource>
    </resources>
    ...
</build>

最后在打包时便可使用如下参数来指定打包的项目情景了:

mvn clean package -Pprod

使用上述命令打包将使应用在运行时将spring.profiles.active设置为prod。

4.7 单元测试

如果单元测试的内容与项目情景有关,则应该在特定的单元测试上使用@ActiveProfile以指定当前测试的情景:

@ActiveProfiles("dev")

4.8 总结

本节中我们讨论了几种设置项目情景的方法,当同时使用上述方法中的多个时,则优先级如下:

  1. web.xml设置(优先级最高)
  2. JVM设置
  3. 环境变量设置
  4. MAVEN属性设置(优先级最低)

5. 默认情景

当Spring中的Bean并没有显式的声明其属性哪个情景,则Spring默认添加到default情景。这也是当项目未声明情景时Spring认证的情景默认值。

我们可以通过设置spring.profiles.default来达到变更默认情景的目的。

6. 获取当前情景

Spring根据当前情景来决定是否启用某个使用了@Profile注解的Bean。有时候,我们可能需要在编码时获取项目当前情景。以下我们介绍几种获取项目当前情景的方法:

6.1 Environment

可以直接注入Environment并调用其上的getActiveProfiles方法来获取当前情景:

    @Autowired
    Environment environment;

    @Override
    public void sentMessage(String phone, String context) {
        for (String active: environment.getActiveProfiles()) {
            System.out.println(active);
        }
     
    }

6.2 @Value

还可以使用@Value来直接获取项目情景:

@Value("${spring.profiles.active}")
private String activeProfile;

需要注意的,当项目当前情景为多个时,比如为:pro、pro-aliyun。那么上述代码注入的activeProfile将为"pro,pro-aliyun",即:使用","分隔了多个情景。

同时,还应该考虑到该值未定义的情况:当spring.profiles.active未定义时,上述代码则会导到处IllegalArgumentException异常。此时,可以为其设置一个默认值以避免异常的发生:

@Value("${spring.profiles.active:}")
private String activeProfile;

上述代码中在spring.profiles.active的后面增加了":" ,用以表示:当应用未设置spring.profiles.active时,使用默认值空字符串来填充activeProfile。

7. 使用示例

比如多数的项目需要的短信验证功能。我们在开发、测试环境中并不需要向手机号上发送真实的短信,而在生产环境中则必须向手机号发送真实的短信。

public interface SmsService {

    /**
     * 发送短信
     * @param phone 接收的手机号
     * @param context 发送的短信文本
     */
    void sentMessage(String phone, String context);
}

那么此时我们便可以建立两个短信实现:

@Service
@Profile("!pro")
public class SmsServiceConsoleImpl implements SmsService {

以上代码实现了:非生产环境时,启用当前的Bean。以实现:在控制台中打印短信验证码。

而在生产环境中使用的真实短信实现则可以如下实现:

@Service
@Profile("pro")
public class SmsServiceImpl implements SmsService {

8. Spring Boot中配置项目情景

Spring Boot支持本文上述所有的配置,同时还提供了一些新特性供我们选择。

比如我们可以通过设置application.properities中的spring.profiles.active来达到快速配置项目情景的目的:

spring.profiles.active=dev

在使用编码进行设置时,还可以通过调用SpringApplication中的setAdditionalProfiles方法来设置项目情景:

    public static void main(String[] args) {
        // 方法一:在run方法执行前,设置spring.profiles.active
//        System.setProperty(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, "dev");

        // 方法二:将原SpringApplication.run(SpringProfilesApplication.class, args)拆分
        // 在执行run方法前执行setAdditionalProfiles方法,来达到变更项目情景的目的
        SpringApplication application = new SpringApplication(SpringProfilesApplication.class);
        application.setAdditionalProfiles("pro");
        application.run(args);
    }

或者利用spring-boot-maven-plugin插件来设置:

<plugins>
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <profiles>
                <profile>dev</profile>
            </profiles>
        </configuration>
    </plugin>
    ...
</plugins>

此时当使用mvn spring-boot:run运行项目时,项目的情景被设置为:dev。

除此以外,Spring Boot带来的最大变化是我们可以将不同情景下的配置单独放到某一个配置文件中,并以application-情景模式名称.properties来命名。比如我们可以为开发、生产模式分别建立application-dev.properties以及application-pro.properties文件。在不同的情景下,Spring Boot将加载特定的配置文件并且使用该配置文件中定义的值来覆盖application.properties中的相同项,同时保留未被覆盖的application.properties中的配置项。

比如在生产环境中指定使用mysql数据库:

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db-example
spring.datasource.username=root
spring.datasource.password=

然后在开发环境中指定使用h2数据为:

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=

引外,Spring还支持yaml格式的配置文件。在yaml格式的配置文件下,可以轻松的使用---在同一个配置文件application.yml中定义不同的情景:

# properties与yml文件并存时,优先使用properties文件
spring:
  profiles:
    active: dev

# 在yaml文件中,可以使用---来分隔不同的情景
---
spring:
  profiles: dev
  application:
    name: yml-name-for-dev

---
spring:
  profiles: pro
  application:
    name: yml-name-for-pro

如上代码定义在application.yml中,与properties需要为不同的情景定义多个配置文件不同,上述代码在一配置文件中定义了dev以及pro两个情景。

9. 总结

本文中我们介绍了如何依据项目的情景来定义Bean,并给出了几种设置项目情景的方法。

项目的情景配置是项目开发中的必修课,我们需要根据不同的情景来启用不同的Bean以适应不同的环境。比如为不同的情景配置不同的数源源、为不同的情景配置不同的短信服务、为不同的情景配置不同的日志服务等。掌握情景的使用以及配置能够使我们在开发过程中有效的规避生产与开发环境的不同,从而提升整个团队的开发效率。这是因为我们再也不需要要因环境变更而注释\启用相关代码块以适应不同的环境了。