Post

Chi tiết Spring @Value và @Autowired

Trong bài viết này, chúng ta sẽ lần lượt xem xét hai chú thích – được chia làm hai phần tương ứng. @Value có thể được sử dụng để đưa các giá trị vào các trường (field) trong các bean do Spring quản lý và nó có thể được áp dụng ở cấp trường (field) hoặc cấp tham số của phương thức/constructor.

Chú thích @Value của Spring còn cung cấp một cách thuận tiện để đưa các giá trị property vào các component. Nó cũng khá hữu ích để cung cấp các giá trị mặc định hợp lý cho các trường hợp có thể không có property. Đó là những gì chúng ta sẽ tập trung vào trong bài viết này - cách chỉ định giá trị mặc định cho chú thích @Value.

Bắt đầu với Spring 2.5, Spring Framework đã giới thiệu tính năng Dependency Injection theo hướng chú thích. Chú thích chính của tính năng này là @Autowired. Nó cho phép Spring resolve và inject các bean cộng tác vào bean của chúng ta.

Với @Autowired, Trước tiên chúng ta sẽ xem xét cách kích hoạt tính năng autowirting và các cách khác nhau để autowire bean. Sau đó, chúng ta sẽ nói về việc giải quyết xung đột bean bằng cách sử dụng chú thích @Qualifier cũng như các trường hợp ngoại lệ đáng chú ý.

1. Spring @Value

Để mô tả các kiểu sử dụng khác nhau cho chú thích này, chúng ta cần cấu hình một lớp cấu hình ứng dụng Spring đơn giản. Chúng ta sẽ cần một property file để xác định các giá trị mà chúng ta muốn đưa vào với chú thích @Value. Và vì vậy, trước tiên chúng ta cần xác định @PropertySource trong lớp cấu hình - với tên property file.

Hãy xác định properties file:

1
2
3
value.from.file=Value got from the file
priority=high
listOfValues=A,B,C

Ví dụ

Là một ví dụ sử dụng cơ bản và hầu như vô dụng, chúng ta chỉ có thể inject “string value” từ chú thích vào trường (field):

1
2
@Value("string value")
private String stringValue;

Sử dụng chú thích @PropertySource cho phép chúng ta làm việc với các giá trị từ property file với chú thích @Value. Trong ví dụ sau, chúng ta nhận được “value from file” được gán cho trường (field):

1
2
@Value("${value.from.file}")
private String valueFromFile;

Chúng ta cũng có thể đặt giá trị từ các property hệ thống với cùng một cú pháp. Hãy giả sử rằng chúng ta đã xác định một property hệ thống có tên là systemValue:

1
2
@Value("${systemValue}")
private String systemValue;

Giá trị mặc định có thể được cung cấp cho các property có thể không được xác định. Trong ví dụ này, giá trị “some default” sẽ được inject:

1
2
@Value("${unknown.param:some default}")
private String someDefault;

Nếu cùng một property được định nghĩa là property hệ thống và trong property file thì property hệ thống sẽ được áp dụng. Giả sử chúng ta có mức ưu tiên property được xác định là property hệ thống với giá trị “System property” và được định nghĩa là thứ khác trong property file. Trong đoạn code sau, giá trị sẽ là “System property”:

1
2
@Value("${priority}")
private String prioritySystemProperty;

Đôi khi chúng ta cần inject vào một loạt các giá trị. Sẽ rất tiện lợi khi xác định chúng dưới dạng các giá trị được phân tách bằng dấu phẩy cho property đơn lẻ trong property file hoặc dưới dạng property hệ thống và đưa vào một mảng. Trong phần đầu tiên, chúng ta đã xác định các giá trị được phân tách bằng dấu phẩy trong listOfValues của property file, do đó trong ví dụ sau, các giá trị mảng sẽ là [“A”, “B”, “C”]:

1
2
@Value("${listOfValues}")
private String[] valuesArray;

Ví dụ nâng cao với SpEL

Chúng ta cũng có thể sử dụng SpEL Expression để lấy value. Nếu chúng ta có property hệ thống được đặt tên là priority thì value của nó sẽ được áp dụng cho trường trong ví dụ tiếp theo:

1
2
@Value("#{systemProperties['priority']}")
private String spelValue;

Nếu chúng ta chưa xác định property hệ thống, thì giá trị null sẽ được gán. Để ngăn điều này, chúng ta có thể cung cấp một giá trị mặc định trong SpEL Expression.

Trong ví dụ sau, chúng ta nhận được giá trị “some default” cho trường nếu property hệ thống không được xác định:

1
2
@Value("#{systemProperties['unknown'] ?: 'some default'}")
private String spelSomeDefault;

Hơn nữa, chúng ta có thể sử dụng một field value từ các bean khác. Giả sử chúng ta có một bean có tên someBean với trường someValue bằng 10. Sau đó, 10 sẽ được gán cho trường trong ví dụ sau:

1
2
@Value("#{someBean.someValue}")
private Integer someBeanValue;

Chúng ta có thể thao tác các property để có được List các giá trị. Trong ví dụ sau, chúng ta nhận được danh sách các giá trị chuỗi A, B và C:

1
2
@Value("#{'${listOfValues}'.split(',')}")
private List<String> valuesList;

Sử dụng @Value với Map

Chúng ta cũng có thể sử dụng chú thích @Value để inject Map property. Trước tiên, chúng ta cần xác định property trong form {key: ‘value '} trong property file của chúng ta:

1
valuesMap={key1: '1', key2: '2', key3: '3'}

Lưu ý rằng các giá trị trong Map phải nằm trong dấu ngoặc kép. Bây giờ chúng ta có thể inject giá trị vào từ property file dưới dạng Map:

1
2
@Value("#{${valuesMap}}")
private Map<String, Integer> valuesMap;

Nếu chúng ta cần lấy giá trị của một key cụ thể trong Map, tất cả những gì chúng ta phải làm là thêm tên của key đó vào biểu thức:

1
2
@Value("#{${valuesMap}.key1}")
private Integer valuesMapKey1;

Nếu chúng ta không chắc liệu Map có chứa một key nhất định hay không, chúng ta nên chọn một biểu thức an toàn hơn sẽ không đưa ra ngoại lệ nhưng đặt giá trị thành null khi không tìm thấy key:

1
2
@Value("#{${valuesMap}['unknownKey']}")
private Integer unknownMapKey;

Chúng ta cũng có thể đặt giá trị mặc định cho các thuộc tính hoặc key có thể không tồn tại:

1
2
3
4
5
@Value("#{${unknownMap : {key1: '1', key2: '2'}}}")
private Map<String, Integer> unknownMap;
 
@Value("#{${valuesMap}['unknownKey'] ?: 5}")
private Integer unknownMapKeyWithDefaultValue;

Các Map cũng có thể được lọc trước khi inject. Giả sử chúng ta chỉ cần lấy những mục có giá trị > 1:

1
2
@Value("#{${valuesMap}.?[value>'1']}")
private Map<String, Integer> valuesMapFiltered;

Chúng ta cũng có thể sử dụng chú thích @Value để inject vào tất cả các thuộc tính hệ thống hiện tại:

1
2
@Value("#{systemProperties}")
private Map<String, String> systemPropertiesMap;

Sử dụng @Value với Constructor Injection

Khi chúng ta sử dụng chú thích @Value, chúng ta không bị giới hạn bởi field injection. Chúng ta cũng có thể sử dụng nó cùng với constructor injection.

Hãy xem điều này trong thực tế:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@PropertySource("classpath:values.properties")
public class PriorityProvider {
 
    private String priority;
 
    @Autowired
    public PriorityProvider(@Value("${priority:normal}") String priority) {
        this.priority = priority;
    }
 
    // standard getter
}

Trong ví dụ trên, chúng ta đưa mức độ ưu tiên trực tiếp vào contructor của PriorityProvider. Lưu ý, chúng ta cũng cung cấp giá trị mặc định trong trường hợp không tìm thấy thuộc tính.

Sử dụng @Value với Setter Injection

Tương tự như đối với constructor injection, chúng ta cũng có thể sử dụng @Value với setter injection.

Hãy xem ví dụ:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@PropertySource("classpath:values.properties")
public class CollectionProvider {
 
    private List<String> values = new ArrayList<>();
 
    @Autowired
    public void setValues(@Value("#{'${listOfValues}'.split(',')}") List<String> values) {
        this.values.addAll(values);
    }
 
    // standard getter
}

Trong đoạn mã trên, chúng ta sử dụng SpEL Expression để đưa danh sách các giá trị vào phương thức setValues.

Trong hướng dẫn này, chúng ta đã kiểm tra các khả năng khác nhau của việc sử dụng chú thích @Value với các thuộc tính đơn giản được xác định trong file với các thuộc tính hệ thống và với các thuộc tính được tính bằng biểu thức SpEL.

Spring @Value Default

Chú thích @Value của Spring cung cấp một cách thuận tiện để inject các giá trị thuộc tính vào các component. Nó cũng khá hữu ích để cung cấp các giá trị mặc định hợp lý cho các trường hợp có thể không có thuộc tính. Đó là những gì chúng ta sẽ tập trung vào trong phần này - cách chỉ định giá trị mặc định cho chú thích @Value Spring.

Hãy xem cú pháp cơ bản để đặt giá trị mặc định cho property String:

1
2
@Value("${some.key:my default value}")
private String stringWithDefaultValue;

Nếu some.key không thể giải quyết được thì stringWithDefaultValue sẽ được đặt thành giá trị mặc định là “my default value”. Tương tự, chúng ta có thể đặt một String có độ dài bằng 0 làm giá trị mặc định:

1
2
@Value("${some.key:})"
private String stringWithBlankDefaultValue;

Primitives

Để đặt giá trị mặc định cho các kiểu nguyên thủy như boolean và int, chúng ta sử dụng literal value:

1
2
3
4
@Value("${some.key:true}")
private boolean booleanWithDefaultValue;
@Value("${some.key:42}")
private int intWithDefaultValue;

Nếu muốn, chúng ta có thể sử dụng các primitive wrapper thay thế bằng cách thay đổi các kiểu thành Boolean và Integer.

Arrays

Chúng ta cũng có thể đưa một danh sách các giá trị được phân tách bằng dấu phẩy vào một mảng:

1
2
3
4
5
@Value("${some.key:one,two,three}")
private String[] stringArrayWithDefaults;
 
@Value("${some.key:1,2,3}")
private int[] intArrayWithDefaults;

Trong @Value đầu tiền, các giá trị “one”, “two”“three” được inject vào làm giá trị mặc định của stringArrayWithDefaults. Trong @Value thứ hai, các giá trị 1, 23 được inject vào làm giá trị mặc định vào intArrayWithDefaults.

Sử dụng SpEL

Chúng ta cũng có thể sử dụng Spring Expression Language (SpEL) để chỉ định một biểu thức và một biểu thức mặc định. Trong ví dụ bên dưới, chúng ta mong đợi some.system.key được đặt làm thuộc tính hệ thống và nếu nó không được đặt, chúng ta muốn sử dụng “my default system property value” làm mặc định:

1
2
@Value("#{systemProperties['some.key'] ?: 'my default system property value'}")
private String spelWithDefaultValue;

Trong phần trên, chúng ta đã xem xét cách chúng ta có thể đặt giá trị mặc định cho thuộc tính mà chúng ta muốn inject vào bằng cách sử dụng chú thích @Value của Spring.

2. Spring @Autowired

Bắt đầu với Spring 2.5, Spring Framework đã giới thiệu tính năng Dependency Injection. Chú thích chính của tính năng này là @Autowired. Nó cho phép Spring resolve và inject các bean cộng tác vào bean của chúng ta.

Trong mục hướng dẫn này, trước tiên chúng ta sẽ xem xét cách kích hoạt tính năng autowiring và các cách khác nhau để autowire bean. Sau đó, chúng ta sẽ nói về việc giải quyết xung đột bean bằng cách sử dụng chú thích @Qualifier, cũng như các trường hợp ngoại lệ.

Enabling @Autowired Annotations

Spring Framework cho phép tự động dependency injection. Nói cách khác, bằng cách khai báo tất cả các bean dependency trong file cấu hình Spring, Spring container có thể autowire các mối quan hệ giữa các bean cộng tác. Điều này được gọi là Spring bean autowiring. Để sử dụng cấu hình dựa trên Java trong ứng dụng, hãy bật annotation-driven injection để tải Spring configuration:

1
2
3
@Configuration
@ComponentScan("com.thecorleone.autowire.sample")
public class AppConfig {}

Ngoài ra, chú thích <context:annotation-config> chủ yếu được sử dụng để kích hoạt dependency injection trong file Spring XML. Hơn nữa, Spring Boot giới thiệu chú thích @SpringBootApplication. Chú thích đơn này tương đương với việc sử dụng @Configuration, @EnableAutoConfiguration@ComponentScan.

Hãy sử dụng chú thích này trong lớp chính của ứng dụng:

1
2
3
4
5
6
@SpringBootApplication
class VehicleFactoryApplication {
    public static void main(String[] args) {
        SpringApplication.run(VehicleFactoryApplication.class, args);
    }
}

Kết quả là khi chúng ta chạy ứng dụng Spring Boot này, nó sẽ tự động quét các component trong package hiện tại và các package con của nó. Do đó, nó sẽ đăng ký chúng trong Application Context của Spring và cho phép chúng ta inject các bean bằng @Autowired.

Sử dụng @Autowired

Sau khi bật injection, chúng ta có thể sử dụng autowiring với properties, setters và constructors.

  • @Autowired on Properties Hãy xem cách chúng ta có thể chú thích một property bằng @Autowired. Điều này giúp loại bỏ nhu cầu về getters và setters.

Đầu tiên, hãy xác định một fooFormatter bean:

1
2
3
4
5
6
@Component("fooFormatter")
public class FooFormatter {
    public String format() {
        return "foo";
    }
}

Sau đó, chúng ta sẽ đưa bean này vào FooService bean bằng cách sử dụng @Autowired bằng cách xác định trường:

1
2
3
4
5
@Component
public class FooService {  
    @Autowired
    private FooFormatter fooFormatter;
}

Kết quả là Spring inject fooFormatter khi FooService được tạo.

  • @Autowired trên Setters

Bây giờ, hãy thử thêm chú thích @Autowired trên phương thức setter. Trong ví dụ sau, phương thức setter được gọi với instance của FooFormatter khi FooService được tạo:

1
2
3
4
5
6
7
public class FooService {
    private FooFormatter fooFormatter;
    @Autowired
    public void setFooFormatter(FooFormatter fooFormatter) {
        this.fooFormatter = fooFormatter;
    }
}
  • @Autowired trên Constructors

Cuối cùng, hãy sử dụng @Autowosystem trên Contructor. Chúng ta sẽ thấy rằng một instance của FooFormatter được Spring inject vào làm đối số cho FooService contructor:

1
2
3
4
5
6
7
public class FooService {
    private FooFormatter fooFormatter;
    @Autowired
    public FooService(FooFormatter fooFormatter) {
        this.fooFormatter = fooFormatter;
    }
}

@Autowired và Dependency tuỳ chọn

Khi một bean đang được xây dựng, các @Autowired dependency sẽ có sẵn. Ngược lại, nếu Spring không thể giải quyết một bean để wired, nó sẽ ném ra một ngoại lệ. Do đó, nó ngăn Spring container khởi chạy với một ngoại lệ:

1
2
3
4
5
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No qualifying bean of type [com.autowire.sample.FooDAO] found for dependency: 
expected at least 1 bean which qualifies as autowire candidate for this dependency.
Dependency annotations: 
{@org.springframework.beans.factory.annotation.Autowired(required=true)}

Để khắc phục điều này, chúng ta cần khai báo một bean thuộc kiểu required như sau:

1
2
3
4
public class FooService {
    @Autowired(required = false)
    private FooDAO dataAccessor; 
}

Autowire định hướng

Theo mặc định, Spring giải quyết các mục @Autowired theo loại. Nếu có nhiều hơn một bean cùng loại trong container, Framework sẽ đưa ra một ngoại lệ nghiêm trọng. Để giải quyết xung đột này, chúng ta cần nói rõ ràng với Spring về bean mà chúng ta muốn inject.

  • Autowiring bằng @Qualifier

Ví dụ, hãy xem cách chúng ta có thể sử dụng chú thích @Qualifier để chỉ ra bean được yêu cầu. Đầu tiên, chúng ta sẽ xác định 2 bean kiểu Formatter:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component("fooFormatter")
public class FooFormatter implements Formatter {
    public String format() {
        return "foo";
    }
}

@Component("barFormatter")
public class BarFormatter implements Formatter {
    public String format() {
        return "bar";
    }
}

Bây giờ chúng ta hãy thử inject một bean Formatter vào lớp FooService:

1
2
3
4
public class FooService {
    @Autowired
    private Formatter formatter;
}

Trong ví dụ trên, có hai triển khai cụ thể của Formatter có sẵn cho Spring container. Kết quả là Spring sẽ ném ra một ngoại lệ NoUniqueBeanDefinitionException khi xây dựng FooService:

1
2
3
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
No qualifying bean of type [com.autowire.sample.Formatter] is defined: 
expected single matching bean but found 2: barFormatter,fooFormatter

Chúng ta có thể tránh điều này bằng cách thu hẹp việc triển khai bằng cách sử dụng chú thích @Qualifier:

1
2
3
4
5
public class FooService {
    @Autowired
    @Qualifier("fooFormatter")
    private Formatter formatter;
}

Khi có nhiều bean cùng loại, bạn nên sử dụng @Qualifier để tránh mơ hồ. Lưu ý rằng giá trị của chú thích @Qualifier phỉa khớp với tên được khai báo trong chú thích @Component của triển khai FooFormatter.

  • Autowiring bằng Qualifier tuỳ chỉnh

Spring cũng cho phép tạo chú thích @Qualifier tùy chỉnh của riêng mình. Để làm như vậy, chúng ta nên cung cấp chú thích @Qualifier với định nghĩa:

1
2
3
4
5
6
7
@Qualifier
@Target({
  ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface FormatterType {  
    String value();
}

Sau đó, chúng ta có thể sử dụng FormatterType trong các triển khai khác nhau để chỉ định giá trị tùy chỉnh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FormatterType("Foo")
@Component
public class FooFormatter implements Formatter {
    public String format() {
        return "foo";
    }
}

@FormatterType("Bar")
@Component
public class BarFormatter implements Formatter {
    public String format() {
        return "bar";
    }
}

Cuối cùng, chú thích @Qualifier tùy chỉnh của chúng ta đã sẵn sàng để sử dụng cho autowiring:

1
2
3
4
5
6
@Component
public class FooService {  
    @Autowired
    @FormatterType("Foo")
    private Formatter formatter;
}

Giá trị được chỉ định trong siêu chú thích @Target hạn chế vị trí áp dụng qualifier - trong ví dụ của chúng ta là field, method, type và parameter.

  • Autowiring theo Name

Spring sử dụng tên của bean làm giá trị định tính mặc định. Nó sẽ kiểm tra container và tìm một bean có tên chính xác như property để autowire.

Do đó, trong ví dụ này - Spring đối chiếu tên property fooFormatter với việc triển khai FooFormatter. Vì thế, nó sẽ inject triển khai cụ thể đó khi xây dựng FooService:

1
2
3
4
public class FooService {
 @Autowired 
private Formatter fooFormatter;
}

Trong mục này, chúng ta đã thảo luận về tính năng autowiring và các cách khác nhau để sử dụng nó. Chúng ta cũng đã kiểm tra các cách giải quyết hai trường hợp ngoại lệ autowiring phổ biến do thiếu bean hoặc inject bean không rõ ràng.

Bài viết mang tính chất “ghi chú, lưu trữ, chia sẻ và phi lợi nhuận”.
Nếu bạn thấy hữu ích, đừng quên chia sẻ với bạn bè và đồng nghiệp của mình nhé!

Happy coding! 😎 👍🏻 🚀 🔥

Đọc thêm:

This post is licensed under CC BY 4.0 by the author.