在本文中,我们将通过添加bean验证和自定义验证器来增强先前的Spring REST Hello World示例 。
使用的技术:
- Spring Boot 2.1.2发布
- Spring5.1.4。发布
- Maven 3
- Java 8
1.控制器
再次检查以前的REST控制器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31package com.mkyong; import com.mkyong.error.BookNotFoundException; import com.mkyong.error.BookUnSupportedFieldPatchException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; @RestController public class BookController { @Autowired private BookRepository repository; // Find @GetMapping("/books") List<Book> findAll() { return repository.findAll(); } // Save @PostMapping("/books") @ResponseStatus(HttpStatus.CREATED) Book newBook(@RequestBody Book newBook) { return repository.save(newBook); } // Find @GetMapping("/books/{id}") Book findOne(@PathVariable Long id) { return repository.findById(id) .orElseThrow(() -> new BookNotFoundException(id)); } //... }
2. Bean验证(休眠验证器)
2.1如果类路径上有任何JSR-303实现(例如Hibernate Validator),则将自动启用Bean验证。 默认情况下,Spring Boot将自动获取并下载Hibernate Validator。
2.2下面的POST请求将被传递,我们需要在book
对象上实现bean验证,以确保诸如name
, author
和price
类的字段不为空。
1
2
3
4@PostMapping("/books") Book newBook(@RequestBody Book newBook) { return repository.save(newBook); }
1curl -v -X POST localhost:8080/books -H "Content-type:application/json" -d "{"name":"ABC"}"
2.3使用javax.validation.constraints.*
注释对bean进行注释。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package com.mkyong; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.validation.constraints.DecimalMin; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.math.BigDecimal; @Entity public class Book { @Id @GeneratedValue private Long id; @NotEmpty(message = "Please provide a name") private String name; @NotEmpty(message = "Please provide a author") private String author; @NotNull(message = "Please provide a price") @DecimalMin("1.00") private BigDecimal price; //... }
2.4将@Valid
添加到@RequestBody
。 完成后,现在启用了bean验证。
1
2
3
4
5
6
7
8
9import javax.validation.Valid; @RestController public class BookController { @PostMapping("/books") Book newBook(@Valid @RequestBody Book newBook) { return repository.save(newBook); } //... }
2.5尝试再次向REST端点发送POST请求。 如果bean验证失败,它将触发MethodArgumentNotValidException
。 默认情况下,Spring将发回HTTP状态400 Bad Request ,但没有错误详细信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19curl -v -X POST localhost:8080/books -H "Content-type:application/json" -d "{"name":"ABC"}" Note: Unnecessary use of -X or --request, POST is already inferred. * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) > POST /books HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.55.1 > Accept: */* > Content-type:application/json > Content-Length: 32 > * upload completely sent off: 32 out of 32 bytes < HTTP/1.1 400 < Content-Length: 0 < Date: Wed, 20 Feb 2019 13:02:30 GMT < Connection: close <
2.6上面的错误响应不是友好的,我们可以捕获MethodArgumentNotValidException
并重写响应,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33package com.mkyong.error; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @ControllerAdvice public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler { // error handle for @Valid @Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { Map<String, Object> body = new LinkedHashMap<>(); body.put("timestamp", new Date()); body.put("status", status.value()); //Get all errors List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(x -> x.getDefaultMessage()) .collect(Collectors.toList()); body.put("errors", errors); return new ResponseEntity<>(body, headers, status); } }
2.7再试一次。 做完了
1
2
3
4
5
6
7
8
9curl -v -X POST localhost:8080/books -H "Content-type:application/json" -d "{"name":"ABC"}" { "timestamp":"2019-02-20T13:21:27.653+0000", "status":400, "errors":[ "Please provide a author", "Please provide a price" ] }
3.路径变量验证
3.1我们还可以将javax.validation.constraints.*
批注直接应用于path变量甚至request参数。
3.2在类级别应用@Validated
,并在路径变量上添加javax.validation.constraints.*
注释,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12import org.springframework.validation.annotation.Validated; import javax.validation.constraints.Min; @RestController @Validated // class level public class BookController { @GetMapping("/books/{id}") Book findOne(@PathVariable @Min(1) Long id) { //jsr 303 annotations return repository.findById(id) .orElseThrow(() -> new BookNotFoundException(id)); } //... }
3.3默认错误消息是好的,只是错误代码500不适合。
1
2
3
4
5
6
7
8curl -v localhost:8080/books/0 { "timestamp":"2019-02-20T13:27:43.638+0000", "status":500, "error":"Internal Server Error", "message":"findOne.id: must be greater than or equal to 1", "path":"/books/0" }
3.4如果@Validated
失败,它将触发ConstraintViolationException
,我们可以像这样重写错误代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package com.mkyong.error; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import javax.servlet.http.HttpServletResponse; import javax.validation.ConstraintViolationException; import java.io.IOException; @ControllerAdvice public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) public void constraintViolationException(HttpServletResponse response) throws IOException { response.sendError(HttpStatus.BAD_REQUEST.value()); } //.. }
1
2
3
4
5
6
7
8curl -v localhost:8080/books/0 { "timestamp":"2019-02-20T13:35:59.808+0000", "status":400, "error":"Bad Request", "message":"findOne.id: must be greater than or equal to 1", "path":"/books/0" }
4.自定义验证器
4.1我们将为author
字段创建一个自定义验证器,仅允许4位作者保存到数据库中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package com.mkyong.error.validator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = AuthorValidator.class) @Documented public @interface Author { String message() default "Author is not allowed."; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
1
2
3
4
5
6
7
8
9
10
11
12package com.mkyong.error.validator; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.Arrays; import java.util.List; public class AuthorValidator implements ConstraintValidator<Author, String> { List<String> authors = Arrays.asList("Santideva", "Marie Kondo", "Martin Fowler", "mkyong"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { return authors.contains(value); } }
1
2
3
4
5
6
7
8
9
10
11package com.mkyong; import com.mkyong.error.validator.Author; import javax.persistence.Entity; import javax.validation.constraints.NotEmpty; //... @Entity public class Book { @Author @NotEmpty(message = "Please provide a author") private String author; //...
4.2测试。 如果自定义验证器失败,它将触发MethodArgumentNotValidException
1
2
3
4
5
6
7
8curl -v -X POST localhost:8080/books -H "Content-type:application/json" -d "{"name":"Spring REST tutorials", "author":"abc","price":"9.99"}" { "timestamp":"2019-02-20T13:49:59.971+0000", "status":400, "errors":["Author is not allowed."] }
5. Spring集成测试
5.1使用MockMvc
测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78package com.mkyong; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.Matchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") public class BookControllerTest { private static final ObjectMapper om = new ObjectMapper(); @Autowired private MockMvc mockMvc; @MockBean private BookRepository mockRepository; /* { "timestamp":"2019-03-05T09:34:13.280+0000", "status":400, "errors":["Author is not allowed.","Please provide a price","Please provide a author"] } */ @Test public void save_emptyAuthor_emptyPrice_400() throws Exception { String bookInJson = "{"name":"ABC"}"; mockMvc.perform(post("/books") .content(bookInJson) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.timestamp", is(notNullValue()))) .andExpect(jsonPath("$.status", is(400))) .andExpect(jsonPath("$.errors").isArray()) .andExpect(jsonPath("$.errors", hasSize(3))) .andExpect(jsonPath("$.errors", hasItem("Author is not allowed."))) .andExpect(jsonPath("$.errors", hasItem("Please provide a author"))) .andExpect(jsonPath("$.errors", hasItem("Please provide a price"))); verify(mockRepository, times(0)).save(any(Book.class)); } /* { "timestamp":"2019-03-05T09:34:13.207+0000", "status":400, "errors":["Author is not allowed."] } */ @Test public void save_invalidAuthor_400() throws Exception { String bookInJson = "{"name":" Spring REST tutorials", "author":"abc","price":"9.99"}"; mockMvc.perform(post("/books") .content(bookInJson) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.timestamp", is(notNullValue()))) .andExpect(jsonPath("$.status", is(400))) .andExpect(jsonPath("$.errors").isArray()) .andExpect(jsonPath("$.errors", hasSize(1))) .andExpect(jsonPath("$.errors", hasItem("Author is not allowed."))); verify(mockRepository, times(0)).save(any(Book.class)); } }
5.2使用TestRestTemplate
测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78package com.mkyong; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.*; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // for restTemplate @ActiveProfiles("test") public class BookControllerRestTemplateTest { private static final ObjectMapper om = new ObjectMapper(); @Autowired private TestRestTemplate restTemplate; @MockBean private BookRepository mockRepository; /* { "timestamp":"2019-03-05T09:34:13.280+0000", "status":400, "errors":["Author is not allowed.","Please provide a price","Please provide a author"] } */ @Test public void save_emptyAuthor_emptyPrice_400() throws JSONException { String bookInJson = "{"name":"ABC"}"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<String> entity = new HttpEntity<>(bookInJson, headers); // send json with POST ResponseEntity<String> response = restTemplate.postForEntity("/books", entity, String.class); //printJSON(response); String expectedJson = "{"status":400,"errors":["Author is not allowed.","Please provide a price","Please provide a author"]}"; assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); JSONAssert.assertEquals(expectedJson, response.getBody(), false); verify(mockRepository, times(0)).save(any(Book.class)); } /* { "timestamp":"2019-03-05T09:34:13.207+0000", "status":400, "errors":["Author is not allowed."] } */ @Test public void save_invalidAuthor_400() throws JSONException { String bookInJson = "{"name":" Spring REST tutorials", "author":"abc","price":"9.99"}"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<String> entity = new HttpEntity<>(bookInJson, headers); //Try exchange ResponseEntity<String> response = restTemplate.exchange("/books", HttpMethod.POST, entity, String.class); String expectedJson = "{"status":400,"errors":["Author is not allowed."]}"; assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); JSONAssert.assertEquals(expectedJson, response.getBody(), false); verify(mockRepository, times(0)).save(any(Book.class)); } private static void printJSON(Object object) { String result; try { result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object); System.out.println(result); } catch (JsonProcessingException e) { e.printStackTrace(); } } }
下载源代码
$ git clone https://github.com/mkyong/spring-boot.git
$ cd spring-rest-validation
$ mvn spring-boot:运行
参考文献
- javax.validation.constraints
- Spring Boot验证功能
- Spring Boot + JUnit 5 + Mockito
- Spring REST Hello World示例
- cURL – POST请求示例
- 维基百科– REST
翻译自: https://mkyong.com/spring-boot/spring-rest-validation-example/
最后
以上就是潇洒羽毛最近收集整理的关于Spring REST验证示例的全部内容,更多相关Spring内容请搜索靠谱客的其他文章。
发表评论 取消回复