分类
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 @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 @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 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以适应不同的环境。比如为不同的情景配置不同的数源源、为不同的情景配置不同的短信服务、为不同的情景配置不同的日志服务等。掌握情景的使用以及配置能够使我们在开发过程中有效的规避生产与开发环境的不同,从而提升整个团队的开发效率。这是因为我们再也不需要要因环境变更而注释\启用相关代码块以适应不同的环境了。

分类
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切面则可以完全关注于查询时间。这或许就是程序开发时关注点分离的具体体现吧。