В этой статье мы улучшим предыдущий пример Spring REST Hello World, добавив проверку компонентов и пользовательский валидатор.
Используемые технологии:
- Пружинный ботинок 2.1.2. ВЫПУСК
- Пружина 5.1.4.ОСВОБОЖДЕНИЕ
- Мавен 3
- Java 8
1. Контроллер
Еще раз просмотрите предыдущий контроллер REST:
package 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") ListfindAll() { 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. Проверка бобов (Валидатор гибернации)
2.1 Проверка компонента будет включена автоматически, если в пути к классу доступна какая-либо реализация JSR-303 (например, валидатор гибернации). По умолчанию Spring Boot автоматически получит и загрузит средство проверки гибернации.
2.2 Приведенный ниже запрос на публикацию будет передан, нам нужно реализовать проверку компонента для объекта книга
, чтобы убедиться, что поля, такие как имя
, автор
и цена
не являются пустыми.
@PostMapping("/books") Book newBook(@RequestBody Book newBook) { return repository.save(newBook); }
curl -v -X POST localhost:8080/books -H "Content-type:application/json" -d "{\"name\":\"ABC\"}"
2.3 Аннотируйте компонент с помощью javax.validation.constraints. * аннотации.
package 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 Добавьте @Действительный
в @Тело запроса
. Готово, проверка компонентов теперь включена.
import javax.validation.Valid; @RestController public class BookController { @PostMapping("/books") Book newBook(@Valid @RequestBody Book newBook) { return repository.save(newBook); } //... }
2.5 Попробуйте снова отправить запрос POST на конечную точку REST. Если проверка компонента завершится неудачно, это вызовет Методаргументнотвалидное исключение
. По умолчанию Spring отправит обратно HTTP-статус 400 Неверный запрос , но без подробностей об ошибке.
curl -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 Приведенный выше ответ на ошибку не является дружественным, мы можем поймать методаргументнотвалидное исключение
и переопределите ответ следующим образом:
package 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
2.7 Повторите попытку. Сделано.
curl -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.. *
примечания к переменной path или даже непосредственно к параметру запроса.
3.2 Примените @Validated
на уровне класса и добавьте ограничения javax.validation.. *
примечания к переменным пути, подобным этому:
import 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 не подходит.
curl -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 Если @Проверено
не удалось, это вызовет Исключение ConstraintViolationException
, мы можем переопределить код ошибки следующим образом:
package 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()); } //.. }
curl -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 Мы создадим пользовательский валидатор для поля автор
, позволяющий сохранять в базе данных только 4 авторам.
package 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 {}; }
package 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{ List authors = Arrays.asList("Santideva", "Marie Kondo", "Martin Fowler", "mkyong"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { return authors.contains(value); } }
package 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 Протестируйте его. Если пользовательский валидатор не удался, он вызовет методаргументнотвалидное исключение
curl -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. Весенний интеграционный тест
5.1 Тест с помощью MockMvc
package 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
package 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); HttpEntityentity = new HttpEntity<>(bookInJson, headers); // send json with POST ResponseEntity 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 entity = new HttpEntity<>(bookInJson, headers); //Try exchange ResponseEntity 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(); } } }
Скачать Исходный Код
Рекомендации
- Рекомендации
- Функции проверки весенней загрузки
- Весенний ботинок + JUnit 5 + Мокито
- Весенний ОТДЫХ Пример Привет, Мир
- Завиток–СТОЛБ примеры запросов
- Википедия – ОТДЫХ
Оригинал: “https://mkyong.com/spring-boot/spring-rest-validation-example/”