Responseentity java что это
Перейти к содержимому

Responseentity java что это

  • автор:

Class ResponseEntity

Extension of HttpEntity that adds an HttpStatusCode status code. Used in RestTemplate as well as in @Controller methods.

In RestTemplate , this class is returned by getForEntity() and exchange() :

ResponseEntity entity = template.getForEntity("https://example.com", String.class); String body = entity.getBody(); MediaType contentType = entity.getHeaders().getContentType(); HttpStatus statusCode = entity.getStatusCode();

This can also be used in Spring MVC as the return value from an @Controller method:

@RequestMapping("/handle") public ResponseEntity handle() < URI location = . ; HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setLocation(location); responseHeaders.set("MyResponseHeader", "MyValue"); return new ResponseEntity("Hello World", responseHeaders, HttpStatus.CREATED); >

Or, by using a builder accessible via static methods:
@RequestMapping(«/handle») public ResponseEntity handle()

  • getStatusCode()
  • RestOperations.getForEntity(String, Class, Object. )
  • RestOperations.getForEntity(String, Class, java.util.Map)
  • RestOperations.getForEntity(URI, Class)
  • RequestEntity

Построение REST сервисов с помощью Spring

REST быстро стал де-факто стандартом для создания web-сервисов, поскольку таким они легки для создания и обработки.

Существует большое обсуждение о том, как REST вписывается в мир микросервисов, но в этом уроке просто описывается создание RESTful сервисов.

Почему REST? Из REST на практике, заимствуется фраза Martin Flower: «Понятие того, что web является доказательством очень масштабируемой распределенной системы, которая работает действительно хорошо и мы можем брать идеи, исходя из того, чтобы создавать системы более легко.» Я думаю, что это довольно веская причина: REST охватывает указания самого web, его архитектуры и его приемуществ.

Какие преимущества? Главным образом все те, какие несет в себе HTTP как платформа сама по себе. Безопасность приложений(шифрование и аутентификация), для которой известно некоторое количество решений. Кеширование встроено в протокол. Маршрутизация, через DNS, гибкая и хорошо известная система уже поддерживается повсеместно.

REST, несмотря на повсеместность использования, не является стандартом, как таковой, а подходом, стилем, ограниечением HTTP протокола. Его реализация может различаться в стиле, подходе. Для клиента такого API это может быть разочарованием. Качество REST сервисов дико варьируется.

Dr. Leonard Richardson собрал воедино модель, которая объясняет различные уровни соответствующих понятий REST и сортирует их. Она описывает 4 уровня, начиная с level 0. Martin Fowler очень хорошо написал про модель

  • Level 0: Swamp of POX — это уровень, где мы просто используем HTTP как транспорт. Вы можете вызвать SOAP технологию. Она использует HTTP, но как транспорт. Стоит также отметить, что вы можете также использовать SOAP поверх чего-то подобного JMS без HTTP. SOAP, соответветственно не является RESTful. Он всего лишь HTTP-aware.
  • Level 1: Resources — на этом уровне сервисы могут использовать HTTP URI для отличия между сущностями в системе. К примеру, вы можете направить запросы на /customers , /users и т.д. XML-RPC является примером Level 1 технологии: он использует HTTP и он может использовать URI для различия точек выхода. В конечном счете, несмотря на то, что XML-RPC не является RESTful, он использует HTTP как транспорт для чего-нибудь ещё(удаленный вызов процедур).
  • Level 2: HTTP Verbs — это уровень, на котором вы хотите быть. Если вы делаете все неправильно с Spring MVC, то вы скорее всего все ещё остановитесь тут. На этом уровне сервисы используют преимущества нативных HTTP возможностей, таким как заголовки, коды статуса, определенные URI и другие. Отсюда мы начнем наше путешествие.
  • Level 3: Hypermedia Controls — это заключительный уровень, к которому мы стремимся. Гипермедиа как практическое применение HATEOAS («HATEOAS» является сокращением от «Hypermedia as the Engine of Application State») шаблона проектирования. Гипермедиа продлевает жизнь сервису, отделяя клиента сервиса от необходимости глубокого знания платформы и топологии сервиса. Она описывает REST сервисы. Сервис может ответить на вопросо о том, какой выл вызов и когда. Мы рассмотрим более глубже в дальнейшем.

Начало работы

Для работы в этом уроке мы будем использовать Spring Boot, который позволяет избежать множество шаблонной разработки. Вы можете начать с Spring Initializr и выбрать флажками соответствующий тип приложения, который будет поддерживаться. В данном случае, мы выбрали создание web приложения. Итак, выберите «Web», а затем нажмите «Generate». Начнется загрузка zip-файла. Распакуйте его. В нем вы найдете простую Maven или Gradle структуру каталогов, содержащим Maven pom.xml и Gradle build.gradle . Большинство людей используют Maven, поэтому в этом уроке будет использован Maven. Кстати, если вы еще не знакомы с Gradle, познакомьтесь. Он очень хорош.

Spring Boot может работать с любой IDE. Вы можете использовать Eclipse, IntelliJ IDEA, Netbeans и другие. Spring Tool Suite является IDE с открытым исходным кодом, основанной на Eclipse, которая предоставляет расширенные возможности Java EE. Она включает возможности для ещё более простого создания Spring приложений. Но присмотритесь к нему, если вы хотите обрести дополнительное преимущество к вашим ключевым действиям. Ниже представлено видео, которое демонстрирует как начать работу с STS и Spring Boot. Это общая инструкция для ознакомления с инструментами.

В предыдущих сериях.

Все наши примеры будут основаны на Spring Boot. Мы будем перепечатывать один и тот же код настройки для каждого примера. Наш пример моделирует простой сервис закладок, который просто собирает URI и описание. Все закладки принадлежат учетной записи пользователя. Эта связь смоделирована с использованием JPA и Spring Data JPA репозиториев в модуле model .

Мы не будем погружаться в слишком большое количество кода. Мы используем две JPA сущности для моделирования записи в БД. Мы используем стандартную SQL БД для сохранения наших записей, чтобы имелась возможность использовать их как можно большему числу клиентов.

Первый класс моделирует учетную запись пользователя.

Поразительно, но ниже приведенный класс является одним из самых шумных — в основном из-за многословности языка Java.

package bookmarks; import com.fasterxml.jackson.annotation.JsonIgnore; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToMany; import java.util.HashSet; import java.util.Set; @Entity public class Account < @OneToMany(mappedBy = "account") private Setbookmarks = new HashSet<>(); @Id @GeneratedValue private Long id; public Set getBookmarks() < return bookmarks; >public Long getId() < return id; >public String getPassword() < return password; >public String getUsername() < return username; >@JsonIgnore public String password; public String username; public Account(String name, String password) < this.username = name; this.password = password; >Account() < // jpa only >>

Каждый Account может не иметь, либо иметь один или множество Bookmark сущностей. Это связь 1:N. Код сущности Bookmark показан ниже:

package bookmarks; import com.fasterxml.jackson.annotation.JsonIgnore; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToOne; @Entity public class Bookmark < @JsonIgnore @ManyToOne private Account account; @Id @GeneratedValue private Long id; Bookmark() < // jpa only >public Bookmark(Account account, String uri, String description) < this.uri = uri; this.description = description; this.account = account; >public String uri; public String description; public Account getAccount() < return account; >public Long getId() < return id; >public String getUri() < return uri; >public String getDescription() < return description; >>

Мы будем использовать два Spring Data JPA репозитория для обработки рутинных взаимодействий с БД. Spring Data репозитории как правило являются интерфейсами с методами, поддерживающими чтение, обновление, удаление и создание записей. Некоторые репозитории также поддерживают разделение на страницы и сортировку. Spring Data реализации основанных на соглашении именования методов в интерфейсе. Существует множество реализаций репозиториев, кроме тех двух. Вы можете использовать Spring Data MongoDB, Spring Data GemFire, Spring Data Cassandra и другие.

Один репозиторий будет управлять нашими Account сущностями, названный как AccountRepository . Один собственный поисковый метод findByUsername будет, в основном, создавать JPA запрос select a from Account a where a.username = :username , запускать его(передавая аргумент username метода как именованный параметр запроса) и возвращать результат.

package bookmarks; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface AccountRepository extends JpaRepository  < OptionalfindByUsername(String username); >

Ниже представлен репозиторий для работы с Bookmark сущностью:

package bookmarks; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Collection; public interface BookmarkRepository extends JpaRepository  < CollectionfindByAccountUsername(String username); >

BookmarkRepository имеет похожий поисковый метод, но разыменовывает поле username связи сущности Bookmark c Account , что в итоге требует присоединения. JPA запрос генерируется примерно таким: SELECT b from Bookmark b WHERE b.account.username = :username .

Наше приложение будет использовать Spring Boot. Spring Boot приложение, это как минимум, точка входа public static void main и аннотация @EnableAutoConfiguration . Это говорит о поддержке Spring Boot там, где это возможно. Наш Application класс также является хорошим местом для помещения разных вещей, например, @Bean определений. Вот как выглядит наш простейший Application.java класс:

package bookmarks; import java.util.Arrays; import java.util.Collection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @Configuration @ComponentScan @EnableAutoConfiguration public class Application < @Bean CommandLineRunner init(AccountRepository accountRepository, BookmarkRepository bookmarkRepository) < return (evt) ->Arrays.asList( "jhoeller,dsyer,pwebb,ogierke,rwinch,mfisher,mpollack,jlong".split(",")) .forEach( a -> < Account account = accountRepository.save(new Account(a, "password")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/1/" + a, "A description")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/2/" + a, "A description")); >); > public static void main(String[] args) < SpringApplication.run(Application.class, args); >>

Как только приложение запустится, Spring Boot вызовет все бины типа CommandLineRunner , передавая им обратный вызов. В данном случае, CommandLineRunner является интерфейсом с одним абстрактным методом, который означает, что — в мире Java 8 — мы можем заменить его определение лямбда выражением. Все примеры в этом уроке будут использовать Java 8. Однако нет никаких оснований не использовать Java 6 или 7, просто замените более короткий синтаксис лямбда более подробным анонимным внутренним классом, реализующим интерфейс.

HTTP — это платформа

HTTP URI является естесственным способом описания иерархии или взаимосвязей. К примеру, мы можем начать наш REST API на уровне учетной записи. Все URI начинаются с имени пользователя. Таким образом, для аккаунта bob мы можем обратиться как /users/bob или просто /bob . Для доступа к списку закладок пользователя мы можем опуститься на уровень ниже(подобно файловой системе) к ресурсу /bob/bookmarks .

REST не предписывает какое-то конкретное представление или кодировку. REST, сокращенно от Representational STate Transfer полагается на HTTP механизм согласования между клиентом и сервисом содержимого для взаимного понимания представления, полученного от сервиса, если это возможно. Существует множество способов для обработки согласования содержимого, но в самом простом случае, клиент отправляет запрос с заголовком Accept , в котором указывается через запятую список mime типов (например: Accept: application/json, application/xml, */* ). Если сервис может обработать какой-либо из этих типов, то он ответит представлением первым из обрабатываемых типов.

Мы можем использовать HTTP глаголы для манипуляции данными, представленными по этим URI.

    GET говорит сервису получить или вернуть ресурс, назначенный по URI. Конечно, делает он это через определенную реализацию. Код сервиса может обращаться к БД, файловой системе, другому web-сервису и др. Клиенту не обязательно знать об этом. Для клиента, все ресурсы являются HTTP ресурсами, и в мире HTTP есть только один способ спросить данные: GET . GET не имеет тела запроса, но обычно возвращает данные. Ответом на HTTP GET запрос /bob/bookmarks/6 может быть примерно таким:

Конечно, некоторые вещи идут не по плану. Скорее всего будет ошибка времени выполнения браузера или сервиса. Мы все получаем раздражающую 404(«Страница не найдена») ошибку, когда пытаемся открыть страницу, которая не существует или ссылка на которую неправильна. 404 является кодом статуса. Она передает информацию о состоянии действия. многие коды статуса разделены различными диапазонами по назначению. Когда вы делаете запрос к web-странице в браузере, он вызывает HTTP GET и если страница отображается, вернется 200 код статуса. 200 означает OK ; вы можете не знать этого, но это так и есть.

  • Коды статуса в 200x диапазоне(200-299) указывают, что действие, которое было запрошено клиентом было получено, понято, принято и обработано успешно
  • Коды статуса в 300x диапазоне(300-399) указывают, что клиент долженпринять дополнительные меры для совершения запроса, такие как перенаправление
  • Коды статуса в 400x диапазоне(400-499) предназначены для случаев, в которых клиент, возможно, допустил ошибку и должен выполнить корректный запрос. Вышеупомянутый 404 является таким примером
  • Коды статуса в 500x диапазоне(500-599) предназначены для случаев, когда произошла ошибка на сервере, несмотря на правильность запроса

Создание REST сервиса

Во первых, REST сервис закладок должен поддерживать выборку и добавление закладок, а также чтение какой-либо конкретной из них. Ниже представлен первоначальный вид нашего REST сервиса:

// tag::runner[] package bookmarks; import java.util.Arrays; import java.util.Collection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @Configuration @ComponentScan @EnableAutoConfiguration public class Application < @Bean CommandLineRunner init(AccountRepository accountRepository, BookmarkRepository bookmarkRepository) < return (evt) ->Arrays.asList( "jhoeller,dsyer,pwebb,ogierke,rwinch,mfisher,mpollack,jlong".split(",")) .forEach( a -> < Account account = accountRepository.save(new Account(a, "password")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/1/" + a, "A description")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/2/" + a, "A description")); >); > public static void main(String[] args) < SpringApplication.run(Application.class, args); >> // end::runner[] @RestController @RequestMapping("//bookmarks") class BookmarkRestController < private final BookmarkRepository bookmarkRepository; private final AccountRepository accountRepository; @RequestMapping(method = RequestMethod.POST) ResponseEntityadd(@PathVariable String userId, @RequestBody Bookmark input) < this.validateUser(userId); return this.accountRepository .findByUsername(userId) .map(account ->< Bookmark result = bookmarkRepository.save(new Bookmark(account, input.uri, input.description)); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setLocation(ServletUriComponentsBuilder .fromCurrentRequest().path("/") .buildAndExpand(result.getId()).toUri()); return new ResponseEntity<>(null, httpHeaders, HttpStatus.CREATED); >).get(); > @RequestMapping(value = "/", method = RequestMethod.GET) Bookmark readBookmark(@PathVariable String userId, @PathVariable Long bookmarkId) < this.validateUser(userId); return this.bookmarkRepository.findOne(bookmarkId); >@RequestMapping(method = RequestMethod.GET) Collection readBookmarks(@PathVariable String userId) < this.validateUser(userId); return this.bookmarkRepository.findByAccountUsername(userId); >@Autowired BookmarkRestController(BookmarkRepository bookmarkRepository, AccountRepository accountRepository) < this.bookmarkRepository = bookmarkRepository; this.accountRepository = accountRepository; >private void validateUser(String userId) < this.accountRepository.findByUsername(userId).orElseThrow( () ->new UserNotFoundException(userId)); > > @ResponseStatus(HttpStatus.NOT_FOUND) class UserNotFoundException extends RuntimeException < public UserNotFoundException(String userId) < super("could not find user '" + userId + "'."); >>

BookmarkRestController является простым Spring MVC компонентом, аннотированным @RestController . @RestController представляет аннотированные методы бина как HTTP точки выхода, используя методанные, предоставленные аннотацией @RequestMapping к каждому из методов. Метод будет вызываться в том случае, если входящий HTTP запрос соответствует требованиям, предусмотренным аннотацией @RequestMapping этого метода.

@RestController , когда он расположен на уромне типа, предоставляет значения по умолчанию для всех методов типа. Каждый отдельный метод может переопределять большинство аннотаций на уровне типа. Некоторые вещи исходят из контекста. К примеру, BookmarkRestController обрабатывает все запросы, которые начинаются с имени пользователя(как bob ), за которым следует /bookmarks . Любые методы в типе, которые затем определяются как URI, как resdBookmark , добавляются к корневому запросу, что будет соответствовать //bookmarks/ . Методы, в которых не указан путь, просто наследуют путь уровня типа. Метод add соответствует URI, указанному на уровне типа, но он только отвечает на HTTP POST запросы.

Слова и являются переменными пути, подстановочными символами. Spring MVC будет извлекать их из URI и делать их доступными как аргументы под тем именем переменной, которое передается методу контроллера и аннотацией @PathVariable . Для HTTP GET запроса по URI /bob/bookmarks/4234 значение агрумента @PathVariable String userId будет «bob» , а для @PathVariable Long bookmarkId будет конвертированное в тип long значение 4324 .

Эти методы контроллера возвращают простые POJO — Collection и Bookmark . Когда HTTP запрос приходит с указанным заголовком Accept , Spring MVC перебирает настройки HttpMessageConverter до тех пор, пока не найдет того, кто сможет конвертировать из типов POJO доменной модели в указанный тип заголовка Accept . Spring Boot автоматически инициализирует HttpMessageConverter , который может конвертировать общий для всех тип Object в JSON, при отсутствии каких-либо других указанных конвертеров. HttpMessageConverter работает в обоих направлениях: тела входящих запросов конвертируются в Java объекты, а Java объекты конвертируются в тела HTTP ответов.

Используйте curl (или ваш браузер), чтобы посмотреть на JSON ответ на запрос http://localhost:8080/jhoeller/bookmarks .

Метод add указывает на параметр типа Bookmark — POJO. Spring MVC будет конвертировать входящий HTTP запрос(содержащий возможно корректный JSON) в POJO с использование соответствующего HttpMessageConverter .

Метод add принимает входящие HTTP запросы, сохраняет их, а затем отправляет обратнно ResponseEntity . ResponseEntity является оберткой для ответа и дополнительно для HTTP заголовков и кода статуса. Метод add отпрвляет обратно ResponseEntity с кодом статуса 201( CREATED ) и заголовком( Location ), чтобы клиент мог воспользоваться ими и изучить, как была создана новая запись. Это немного похоже на извлечение первичного ключа после сохранения записи в БД.

Существуют не принимаемые пути. По умолчанию, Spring Boot устанавливает довольно обширный набор реализаций HttpMessageConverter , подходящие для использования широкого круга задач, но также легко добавть к нему поддержку и для других, возможно более компактных, эффектиных(для сети) форматов(как Google Protocol Buffers implementation in Spring 4.1) с использованием обычной Spring MVC конфигурации.

Spring MVC нативно поддерживаетзагрузку файлов через аргументы контроллера типа MultipartFile multipartFile .

В Spring MVC это делается простым написанием сервисно-ориентированного кода, в котором фигурирует чистый HttpServlet API. Этот код может быть легко протестирован, расширен через Spring AOP. Мы посмотрим, как протестировать эти Spring MVC компоненты в следующей секции.

Использование HTTP для указания ошибок

Все методы в REST сервисе вызывают validateUser для проверки, что запрашиваемый пользователь существует, а если нет, то бросать UserNotFoundException . Это исключение содержит Spring MVC аннотацию @ResponseStatus(HttpStatus.NOT_FOUND) , которая говорит Spring MVC отправлять обратно код HTTP статуса(404) при каждом срабатывании исключения. Это действительно хороший прием: вы можете думать в терминах вашего бизнес домена в вашем коде и вы получите умную обработку, которая прекрасно соответствует тому, как HTTP указывает на ошибки клиента, используя коды статуса. Spring MVC предоставляет множество различных мест, чтобы аккуратно обработать ошибки.

Тестирование REST сервиса

Spring MVC предоставляет хорошоую поддержку для тестирования HTTP точек выхода. Он предоставляет очень хорошее нечто среднее между unit-тестированием и интеграционным тестированием в том, что он позволяет вам поднять весь Spring MVC механизм на основе DispatcherServlet , включая валидаторы, HttpMessageConverter и многое другое, а затем запустить тесты без фактического запуска самого HTTP сервиса: как убить двух зайцев одновременно! От интеграционного тестирования в том, что вы заботитесь о фактической реализации логики, от unit-тестирования — в том, что вы не дожидаетесь инициализации web-сервера и начинаете обслуживать запросы.

ниже показан unit-тест для ReservationRestController . Он должен быть знаком каждому, кто писал до этого тесты на JUnit. На верху мы используем аннотацию @SpringApplicationConfiguration(classes = Application.class) (из Spring Boot), чтобы сказать SpringJUnit4ClassRunner , где он должен взять информацию о Spring приложении для тестирования. Аннотация @WebAppConfiguration говорит JUnit, что это unit-тест для Spring MVC web-компонентов и поэтому должен выполняться под WebApplicationContext , а не стандартной ApplicationContext реализации.

package bookmarks; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; /** * @author Josh Long */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class BookmarkRestControllerTest < private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8")); private MockMvc mockMvc; private String userName = "bdussault"; private HttpMessageConverter mappingJackson2HttpMessageConverter; private Account account; private ListbookmarkList = new ArrayList<>(); @Autowired private BookmarkRepository bookmarkRepository; @Autowired private WebApplicationContext webApplicationContext; @Autowired private AccountRepository accountRepository; @Autowired void setConverters(HttpMessageConverter[] converters) < this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter( hmc ->hmc instanceof MappingJackson2HttpMessageConverter).findAny().get(); Assert.assertNotNull("the JSON message converter must not be null", this.mappingJackson2HttpMessageConverter); > @Before public void setup() throws Exception < this.mockMvc = webAppContextSetup(webApplicationContext).build(); this.bookmarkRepository.deleteAllInBatch(); this.accountRepository.deleteAllInBatch(); this.account = accountRepository.save(new Account(userName, "password")); this.bookmarkList.add(bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/1/" + userName, "A description"))); this.bookmarkList.add(bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/2/" + userName, "A description"))); >@Test public void userNotFound() throws Exception < mockMvc.perform(post("/george/bookmarks/") .content(this.json(new Bookmark())) .contentType(contentType)) .andExpect(status().isNotFound()); >@Test public void readSingleBookmark() throws Exception < mockMvc.perform(get("/" + userName + "/bookmarks/" + this.bookmarkList.get(0).getId())) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$.id", is(this.bookmarkList.get(0).getId().intValue()))) .andExpect(jsonPath("$.uri", is("http://bookmark.com/1/" + userName))) .andExpect(jsonPath("$.description", is("A description"))); >@Test public void readBookmarks() throws Exception < mockMvc.perform(get("/" + userName + "/bookmarks")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$", hasSize(2))) .andExpect(jsonPath("$[0].id", is(this.bookmarkList.get(0).getId().intValue()))) .andExpect(jsonPath("$[0].uri", is("http://bookmark.com/1/" + userName))) .andExpect(jsonPath("$[0].description", is("A description"))) .andExpect(jsonPath("$[1].id", is(this.bookmarkList.get(1).getId().intValue()))) .andExpect(jsonPath("$[1].uri", is("http://bookmark.com/2/" + userName))) .andExpect(jsonPath("$[1].description", is("A description"))); >@Test public void createBookmark() throws Exception < String bookmarkJson = json(new Bookmark( this.account, "http://spring.io", "a bookmark to the best resource for Spring news and information")); this.mockMvc.perform(post("/" + userName + "/bookmarks") .contentType(contentType) .content(bookmarkJson)) .andExpect(status().isCreated()); >protected String json(Object o) throws IOException < MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage(); this.mappingJackson2HttpMessageConverter.write( o, MediaType.APPLICATION_JSON, mockHttpOutputMessage); return mockHttpOutputMessage.getBodyAsString(); >>

Во-первых, на что нужно обратить внимание — это метод setup , аннотированный @Before . Этот метод инициализирует MockMvc , которому требуется ссылка на WebApplicationContext приложения. MockMvc является ключевой фигурой: все тесты будут постоянно использовать переменную типа MockMvc для имитации HTTP запросов к сервису. И. это всё! Посмотрите на любой другой из тестов. Мы можем использовать статический импорт из org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* для сцепления вместе HTTP запросов и проверки их ответов.

Для всех тестов указаны application/json content-type и ответы ожидаются такого же типа. Тесты используют метод MockMvcResultMatchers#jsonPath для проверки структуры и содержания JSON ответов, что в свою очередь использует Jayway JSON Path API для запуска обхода с тиле X-Path по JSON структурам, как мы делаем в различных частях unit-тестов.

Создание HATEOAS REST сервиса

Текущий первоначальный вид API работает очень хорошо. Если этот сервис хорошо документирован, он будет работоспособен для REST клиентов написанных на множестве различных языков. Это чистый API в том, что мспользуются некоторые из примитивов, которые предоставляет HTTP, понятным способом. Особенностью API является соответствие принципу единого интерфейса. Каждое сообщение включает достаточно информации для описания того, как обрабатывать сообщение. К примеру, клиент может решить, какой парсер будет вызван на основе заголовка Content-type в сообщении запроса. Состояние системы соответствует идентификации уникальности URI ресурса. Изменение состояния осуществляется через HTTP глаголы ( POST , GET , DELETE , PUT и др.). Таким образом, когда клиент хранит представление ресурса, включая метаданные, он имеет достаточно информации для изменения или удаления ресурса.

Но мы можем сделать лучше. Сервисы остаются соответствовать задачам, но отсутствует. надежность. Как сказано в Википедии: Клиенты должны знать API. Изменения в API отталкивают клиентов и они не обращаются к документации сервиса. Гипермедиа как двигатель состояния приложения(a.k.a. HATEOAS) является ещё одним ограничением, который адресован и удаляет эту нестыковку. Клиенты создают состояния переходов только через действия, которые динамически идентифицируются в гипермедиа сервером.

Давайте посмотрим на исправленный вариант API, который показан ниже, но на этот раз с использованием Spring HATEOAS. Это сделано для того, чтобы рассказать, что Spring HATEOAS делает проще предоставление ссылок, матаданных о полезной нагрузке, которая будет возвращена клиенту. В принципе, все что мы будем делать, это обертывать наш ответ полезной нагрузкой, используя Spring HATEOAS тип ResourceSupport . Этот тип аккумулирует объекты Link , которые описывают ресурсы. К примеру, ресурс описывает счет в e-commerce решении, который может иметь ссылку на ресурс заказа по счету, ссылку на текущую карту оплаты и ссылку, которая может быть использована для повторного извлечения состояния ресурса.

package bookmarks; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.Link; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; import org.springframework.hateoas.VndErrors; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; @Configuration @ComponentScan @EnableAutoConfiguration public class Application < @Bean CommandLineRunner init(AccountRepository accountRepository, BookmarkRepository bookmarkRepository) < return (evt) ->Arrays.asList("jhoeller,dsyer,pwebb,ogierke,rwinch,mfisher,mpollack,jlong".split(",")) .forEach(a -> < Account account = accountRepository.save(new Account(a, "password")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/1/" + a, "A description")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/2/" + a, "A description")); >); > public static void main(String[] args) < SpringApplication.run(Application.class, args); >> class BookmarkResource extends ResourceSupport < private final Bookmark bookmark; public BookmarkResource(Bookmark bookmark) < String username = bookmark.getAccount().getUsername(); this.bookmark = bookmark; this.add(new Link(bookmark.getUri(), "bookmark-uri")); this.add(linkTo(BookmarkRestController.class, username).withRel("bookmarks")); this.add(linkTo(methodOn(BookmarkRestController.class, username).readBookmark(username, bookmark.getId())).withSelfRel()); >public Bookmark getBookmark() < return bookmark; >> @RestController @RequestMapping("//bookmarks") class BookmarkRestController < private final BookmarkRepository bookmarkRepository; private final AccountRepository accountRepository; @RequestMapping(method = RequestMethod.POST) ResponseEntityadd(@PathVariable String userId, @RequestBody Bookmark input) < this.validateUser(userId); return accountRepository.findByUsername(userId) .map(account ->< Bookmark bookmark = bookmarkRepository.save(new Bookmark(account, input.uri, input.description)); HttpHeaders httpHeaders = new HttpHeaders(); Link forOneBookmark = new BookmarkResource(bookmark).getLink("self"); httpHeaders.setLocation(URI.create(forOneBookmark.getHref())); return new ResponseEntity<>(null, httpHeaders, HttpStatus.CREATED); > ).get(); > @RequestMapping(value = "/", method = RequestMethod.GET) BookmarkResource readBookmark(@PathVariable String userId, @PathVariable Long bookmarkId) < this.validateUser(userId); return new BookmarkResource(this.bookmarkRepository.findOne(bookmarkId)); >@RequestMapping(method = RequestMethod.GET) Resources readBookmarks(@PathVariable String userId) < this.validateUser(userId); ListbookmarkResourceList = bookmarkRepository.findByAccountUsername(userId) .stream() .map(BookmarkResource::new) .collect(Collectors.toList()); return new Resources(bookmarkResourceList); > @Autowired BookmarkRestController(BookmarkRepository bookmarkRepository, AccountRepository accountRepository) < this.bookmarkRepository = bookmarkRepository; this.accountRepository = accountRepository; >private void validateUser(String userId) < this.accountRepository.findByUsername(userId) .orElseThrow(() ->new UserNotFoundException(userId)); > > @ControllerAdvice class BookmarkControllerAdvice < @ResponseBody @ExceptionHandler(UserNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) VndErrors userNotFoundExceptionHandler(UserNotFoundException ex) < return new VndErrors("error", ex.getMessage()); >> class UserNotFoundException extends RuntimeException < public UserNotFoundException(String userId) < super("could not find user '" + userId + "'."); >>

Эти Link соответствуют тем же элементам , которые часто используются в HTML страницах и CSS таблицах. Они имеют атрибуты href и rel . Атрибут href указывает на местоположение CSS таблицы, а rel говорит клиенту(браузеру) почему этот ресурс важен(потомучто эта таблица используется для отображения страницы). Поэтому не составляет труда перевести элемент link в JSON.

На данном этапе остается только заменить Bookmark на BookmarkResource .

Улучшенная обработка ошибок с использованием VndErrors

HATEOAS передает клиентам улучшенные метаданные о самом сервисе. Мы можем также улучшить ситуацию обработки ошибок. Коды HTTP статусов 400-499, к примеру, говорят клиенту, что клиент сделал что-то неправильнное, коды 500-599 говорят о чем-то нехорошем на стороне сервера. Если вы знаете ваши коды статусов, то это может быть началом к пониманию, как работать с API. Но мы можем сделать лучше. В конце концов, перед тем, как клиент запустится, кому-то необходимо разработать его и сообщения об ошибках могут быть бесценными для понимания API. Ошибки могут быть обработаны последовательным способом даже лучше!

VndError является популярным, де-факто стандартом mime-типом для описания кодировки ошибок клиента REST сервиса. Он работает также с ошибками 40х и 50х. Spring HATEOAS предоставляет типы VndError , которые вы можете использовать для сообщения об ошибке обратнно клиенту.

В наш исправленный сервис вводится новый класс BookmarkControllerAdvice , который использует Spring MVC аннотацию @ControllerAdvice . Использование этой аннотации является полезным способом для распределения общих проблем конфигурации, как обработка ошибки, в отдельных местах, отдельно от конкретного Spring MVC контроллера. Spring MVC, к примеру, определяет @ExceptionHandler методу, который привязывает определенный метод обработчика к любому вызову Exception или коду HTTP статуса. Здесь мы говорим Spring MVC, что любой код, который бросает UserNotFoundException , как и ранее, должен быть обработан методом userNotFoundExceptionHandler . Этот метод просто оборачивает Exception в VndErrors и возвращает его клиенту. В этом примере мы убрали аннотацию @ResponseStatus из исключения и объединили в @ControllerAdvice . @ControllerAdvice является удобным способом для объединения логики всех видов и, в отличие от аннотированных типов исключений, возможно использование для каждого из типов исключения, к которому у вас нет исходного кода.

Обеспечение защиты REST сервиса

До сих пор мы исходили из предположения, что все клиенты надежны и что они должны обладать полным доступом ко всем данным. На самом деле такое редко бывает. Открытое REST API незащищено. Хотя это нетрудно исправить. Spring Security предоставляет фундамент для защиты доступа к приложению. Spring Security необходимо иметь представление о пользователях вашего приложения и их правах. Эти права отвечают на вопрос: что может пользователь приложения видеть или делать?

Основой Spring Security является интерфейс UserDetailsService , который выполняет одну задачу: по переданному имени пользователя, исполняет реализацию UserDetails , которая должна отвечать на запросы актуальности учетной записи, её пароля, имени пользователя и прав(представленными экземплярами org.springframework.security.core.GrantedAuthority ).

package org.springframework.security.core.userdetails; public interface UserDetailsService

Spring Security предоставляет множество реализаций для адаптации существующих поставщиков идентификации, таких как Active Directory, LDAP, pam , CAAS и др. Spring Social предоставляет хорошую интеграцию с такими сервисами аутентификации на основе OAuth, как Facebook, Twitter и др.

Наш пример уже имеет понятие Account , поэтомы мы можем просто приспособить его к нашей реализации UserDetailsService , как показано ниже в классе WebSecurityConfiguration . Spring Security будет опрашивать этот UserDetailsService , есть ли у него какой-либо ответ на запрос аутентификации.

Авторизация и аутентификация клиента в Open Web с использование Spring Security OAuth

Мы можем выпонить запросы аутентификации клиента бесчисленным количеством способов. Клиенты могут отправлять, к примеру имя пользователя и пароль в каждом HTTP-запросе. Они могут передавать х509 сертификат в каждом запросе. В действительности существует множество подходов, которые могут быть использованы здесь.

Наш API предназначен для работы поверх open-web. Он предназначен для использования всевозможных HTML5 и нативных мобильных и настольных клиентов, которые мы намерены создать. Мы будем использовать разнообразные клиенты с различной степенью защиты и любое решение какое бы мы не выбрали, должно соответствовать этому. Мы должны также отделить имя пользователя и пароль из сессии приложения. В итоге, если сбросить пароль на Twitter, не хочется, чтобы клиенту пришлось повторно аутентифицироваться. С другой стороны, если кто-то угонит его у одного из наших клиентов(к примеру, если пользователь потерял телефон), то мы не хотим, чтобы злоумышленник смог заблокировать нашего пользователя.

OAuth предоставляет прозрачный способ обработки этих проблем. Без сомнения, вы уже использовали OAuth. Одним из распостраненных случаев — это когда устанавливается Facebook плагин или игра. Обычно он состоит из следующих этапов:

  • пользователь находит игру или часть функциональности, которой требуется доступ данным пользователя, к примеру, «Sign in With Facebook»
  • пользователь нажимает «install» или «add» и затем перенаправляется на доверенный домен(Facebook.com), где пользователю будет предложено предоставить определенные разрешения
  • пользователь подтверждает эти разрешения и впоследствии перенаправляется обратно к приложению с полученным ключом. Оно будет использовать этот ключ в запросах к Facebook от вашего имени

В этом примере любой старый клиент может обратиться к Facebook, а по окончании иметь ключ доступа. Этот ключ доступа передается через все последующие REST запросы, наподобие HTTP cookie. Имя пользователи и пароль не нужно будет передавать и клиент может кешировать ключ доступа на определенный или неопределенный период. Пользователям клиента нет необходимости повторно аутентифицироваться каждый раз при открытии приложения. А ещё лучше, ключ доступа определять к каждому клиенту. Это может быть использовано для определения того, что одному клиенту требуется больше разрешений, чем другим. В этом и есть преимущество, состоящее в том, что любая конфиденциальная информация, как имя пользователь и пароль, никогда не будет в открытом виде у ненадежных приложений, которые могут использовать её в злонамеренных целях. Мы можем быть уверены в том, что любой клиент будет дружественным. OAuth поддерживает простейший этап проверки подлинности пользователя( обычно отправляя имя пользователя и пароль) от клиента и сервис возвращает ключ доступа OAuth без необходимости перенаправления на доверенный домен. Этот подход мы и возьмем: результатом будет то, что наши клиенты будут иметь ключ доступа, который отделен от имени пользователя и пароля, и который может быть использован для присвоения различных уровней защиты различным клиентам.

Настройка OAuth для вашего приложения проста. Конфигурационный класс OAuth2Configuration описывает один клиент(здесь гипотетический Android клиент), которому необзодим ROLE_USER и write . Spring Security OAuth будет читать эту информацию из AuthenticationManager , который к итоге настроен на использовании нашей собственной UserDetailsService реализации.

OAuth очень гибок. Вы можете, к примеру, развернуть сервер авторизации, общий для множества REST API. В этом случае, наша OAuth реализация живет рядом с нашим REST API закладок, т.к. они одни и те же. Именно поэтому мы использовали оба @EnableResourceServer и @EnableAuthorizationServer в одном и том же конфигурационном классе.

Вы сказали что-то про Open Web?

Мы ожидаем, что некоторые из клиентов нашего сервиса будут на HTML5. Они будут обращаться к нашему REST API с различных доменов, а в большинстве браузеров это идет в разрез политики безопасности cross-site-scripting. Мы можем явно включить XSS для заранее известных клиентов, вставляя CORS (cross-origin request scripting) заголовки. Эти заголовки, если они присутствуют в ответах сервиса, указывают браузеру, что запросы происхождения, форм и конфигурации описаны в заголовках, разрешенных даже в различных доменах. Наш API оформлен простым javax.servlet.Filter , который добавляет эти заголовки в каждый запрос. В примере мы делегируем свойству tagit.origin , если предоставлен или по умолчанию http://localhost:9000 , где у нас может, к примеру, JavaScript клиент осуществлять запросы к сервису. Мы могли бы легко прочитать эту информацию из хранилица. Мы указали только один источник, но можно также легко указать различное количество клиентов.

Использование HTTPS (SSL/TLS) для предотвращения Man-in-the-Middle атак

Spring Boot предоставляет встроенный web-сервер(Apache Tomcat по умолчанию), который может быть настроен программно делать что-либо, что и отдельный Apache Tomcat web-сервер может делать. Раньше требовалось выполнить несколько нудных шагов для настройки HTTPS (SSL/TLS). Теперь, Spring Boot делает это супер просто. Для начала, создайте следующее:

# Настройка сервера на запуск с SSL/TLS и использования HTTPS server.port = 8443 server.ssl.key-store = classpath:tomcat.keystore server.ssl.key-store-password = password server.ssl.key-password = password

Этот файл свойств активируется, только когда приложение запущено и SPRING_PROFILES_ACTIVE настроен с профилем https.

HTTPS требует подписанный сертификат, который мы предъявляем, используя значения свойств. Для этого мы можем использовать из JDK keytool:

$ keytool -genkey -alias bookmarks -keyalg RSA -keystore src/main/resources/tomcat.keystore Enter keystore password: password Re-enter new password: password What is your first and last name? [Unknown]: Josh Long What is the name of your organizational unit? [Unknown]: Spring Team What is the name of your organization? [Unknown]: Pivotal What is the name of your City or Locality? [Unknown]: IoT What is the name of your State or Province? [Unknown]: Earth What is the two-letter country code for this unit? [Unknown]: US Is CN=Josh Long, OU=Spring Team, O=Pivotal, L=IoT, ST=Earth, C=US correct? [no]: yes Enter key password for (RETURN if same as keystore password):

Файлы ключей ДОЛЖНЫ быть в файловой системе втроенного Tomcat, чтобы прочесть их. Они НЕ могут быть в виде JAR файлов.

Создадуться хранилище ключей в src/main/resources . Теперь, если вы запустите приложение, то оно будет использовать хранилище ключей для запуска встроенного контейнера сервлетов Tomcat с использованием SSL/TLS.

как говорилось ранее, этот режим доступен только для использования профиля Spring https . Это означает, что вы можете запускать и тестировать приложение без примененного профиля, либо также просто переключившись на него.

Собираем все вместе

package bookmarks; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; import java.io.IOException; import java.net.URI; import java.security.Principal; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.embedded.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.Link; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; import org.springframework.hateoas.VndErrors; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; // // curl -X POST -vu android-bookmarks:123456 http://localhost:8080/oauth/token -H "Accept: application/json" -d "password=password&username=jlong&grant_type=password&scope=write&client_secret=123456&client_id=android-bookmarks" // curl -v POST http://127.0.0.1:8080/tags --data "tags=cows,dogs" -H "Authorization: Bearer 66953496-fc5b-44d0-9210-b0521863ffcb" @Configuration @ComponentScan @EnableAutoConfiguration public class Application < // CORS @Bean FilterRegistrationBean corsFilter( @Value("$") String origin) < return new FilterRegistrationBean(new Filter() < public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException < HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; String method = request.getMethod(); // this origin value could just as easily have come from a database response.setHeader("Access-Control-Allow-Origin", origin); response.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE"); response.setHeader("Access-Control-Max-Age", Long.toString(60 * 60)); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader( "Access-Control-Allow-Headers", "Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization"); if ("OPTIONS".equals(method)) < response.setStatus(HttpStatus.OK.value()); >else < chain.doFilter(req, res); >> public void init(FilterConfig filterConfig) < >public void destroy() < >>); > @Bean CommandLineRunner init(AccountRepository accountRepository, BookmarkRepository bookmarkRepository) < return (evt) ->Arrays.asList( "jhoeller,dsyer,pwebb,ogierke,rwinch,mfisher,mpollack,jlong".split(",")) .forEach( a -> < Account account = accountRepository.save(new Account(a, "password")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/1/" + a, "A description")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/2/" + a, "A description")); >); > public static void main(String[] args) < SpringApplication.run(Application.class, args); >> @Configuration class WebSecurityConfiguration extends GlobalAuthenticationConfigurerAdapter < @Autowired AccountRepository accountRepository; @Override public void init(AuthenticationManagerBuilder auth) throws Exception < auth.userDetailsService(userDetailsService()); >@Bean UserDetailsService userDetailsService() < return (username) ->accountRepository .findByUsername(username) .map(a -> new User(a.username, a.password, true, true, true, true, AuthorityUtils.createAuthorityList("USER", "write"))) .orElseThrow( () -> new UsernameNotFoundException("could not find the user '" + username + "'")); > > @Configuration @EnableResourceServer @EnableAuthorizationServer class OAuth2Configuration extends AuthorizationServerConfigurerAdapter < String applicationName = "bookmarks"; // This is required for password grants, which we specify below as one of the // . @Autowired AuthenticationManagerBuilder authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception < // Workaround for https://github.com/spring-projects/spring-boot/issues/1801 endpoints.authenticationManager(new AuthenticationManager() < @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException < return authenticationManager.getOrBuild().authenticate(authentication); >>); > @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception < clients.inMemory().withClient("android-" + applicationName) .authorizedGrantTypes("password", "authorization_code", "refresh_token") .authorities("ROLE_USER").scopes("write").resourceIds(applicationName) .secret("123456"); >> class BookmarkResource extends ResourceSupport < private final Bookmark bookmark; public BookmarkResource(Bookmark bookmark) < String username = bookmark.getAccount().getUsername(); this.bookmark = bookmark; this.add(new Link(bookmark.uri, "bookmark-uri")); this.add(linkTo(BookmarkRestController.class, username).withRel("bookmarks")); this.add(linkTo( methodOn(BookmarkRestController.class, username).readBookmark(null, bookmark.getId())).withSelfRel()); >public Bookmark getBookmark() < return bookmark; >> @RestController @RequestMapping("/bookmarks") class BookmarkRestController < private final BookmarkRepository bookmarkRepository; private final AccountRepository accountRepository; @RequestMapping(method = RequestMethod.POST) ResponseEntityadd(Principal principal, @RequestBody Bookmark input) < String userId = principal.getName(); this.validateUser(userId); return accountRepository .findByUsername(userId) .map(account ->< Bookmark bookmark = bookmarkRepository.save(new Bookmark(account, input.uri, input.description)); HttpHeaders httpHeaders = new HttpHeaders(); Link forOneBookmark = new BookmarkResource(bookmark).getLink("self"); httpHeaders.setLocation(URI.create(forOneBookmark.getHref())); return new ResponseEntity<>(null, httpHeaders, HttpStatus.CREATED); >).get(); > @RequestMapping(value = "/", method = RequestMethod.GET) BookmarkResource readBookmark(Principal principal, @PathVariable Long bookmarkId) < String userId = principal.getName(); this.validateUser(userId); return new BookmarkResource(this.bookmarkRepository.findOne(bookmarkId)); >@RequestMapping(method = RequestMethod.GET) ResourcesreadBookmarks(Principal principal) < String userId = principal.getName(); this.validateUser(userId); ListbookmarkResourceList = bookmarkRepository .findByAccountUsername(userId).stream().map(BookmarkResource::new) .collect(Collectors.toList()); return new Resources(bookmarkResourceList); > @Autowired BookmarkRestController(BookmarkRepository bookmarkRepository, AccountRepository accountRepository) < this.bookmarkRepository = bookmarkRepository; this.accountRepository = accountRepository; >private void validateUser(String userId) < this.accountRepository.findByUsername(userId).orElseThrow( () ->new UserNotFoundException(userId)); > > @ControllerAdvice class BookmarkControllerAdvice < @ResponseBody @ExceptionHandler(UserNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) VndErrors userNotFoundExceptionHandler(UserNotFoundException ex) < return new VndErrors("error", ex.getMessage()); >> @SuppressWarnings("serial") class UserNotFoundException extends RuntimeException < public UserNotFoundException(String userId) < super("could not find user '" + userId + "'."); >>

С Spring Security OAuth мы можем немного доработать наш API. Не имеет смысла иметь разбросанные userId по всему URI, потому что контекст пользователя предполагает безопасный доступ к нашим точкам выхода. Вместо требования userId в пути, безопасные методы обработчика Spring MVC ожидают javax.security.Principal , который Spring Security внедряет от нашего имени. Он имеет метод javax.security.Principal#getName , который можно использовать вместо userId .

Заключение

REST является наиболее естесственным способом для повсеместного, распределенного взаимодействия с клиентами. Он работает потому что работает HTTP. Когда вы поймете, как REST встраивается в приложение, тогда вам не трудно будет представить архитектуру с многочисленными, отдельновзятыми API, открытыми поверх REST, с балансировкой нагрузки, каким и должен быть любой другой HTTP сервис. Не удивительно, что REST(часто, но не обязательно) образует важнейшую основу для микросервисов, которые рассмотрены на протяжении всего урока.

С оригинальным текстом урока вы можете ознакомиться на spring.io.

ResponseEntity<> должен вернуть несколько объектов. Как это осуществить?

Необходимо вернуть детали по id продукта и их количество. Прошу оценить код.

@RestController @RequestMapping("/api/v1/details") public class DetailRestControllerV1 < private final DetailService detailService; public DetailRestControllerV1(DetailService detailService) < this.detailService = detailService; >@GetMapping("/byproduct/") public ResponseEntity getDetailsByProduct(@PathVariable("id") Long productId) < if (productId == null) < return new ResponseEntity<>(HttpStatus.BAD_REQUEST); > List details = detailService.findDetailsByProductId(productId); if (details.isEmpty()) < return new ResponseEntity<>(HttpStatus.NOT_FOUND); > int countDetails = details.size(); DetailsAndDetailsCountContainer container = new DetailsAndDetailsCountContainer(); container.setDetails(details); container.setCountDetails(countDetails); return new ResponseEntity<>(container, HttpStatus.OK); > 

> Сам объект

@Data public class DetailsAndDetailsCountContainer < private Listdetails; private int countDetails; > 

Насколько правильное это решение и есть ли альтернативы?

Отслеживать

задан 11 фев 2021 в 9:16

Николай дерягин Николай дерягин

7 1 1 бронзовый знак

Вы изобрели обычный wrapper, что является правильным и достаточно стандартным подходом.

11 фев 2021 в 9:41

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

Если на другой стороне полноценный JSON парсер (Java, JavaScript, TypeScript. ), то можно возвращать ResponseEntity> , список на той стороне десериализуется в список или массив, в зависимости от языка, а у него будет длина.

public class DataList  < private Listitems; private int count; DataList(List items) < this.items = items; this.count = items.size(); >> ResponseEntity> getDetailsByProduct(. ) < . return new ResponseEntity<>(new DataList(details), HttpStatus.OK); > 

Изучаем ResponseEntity и избавляемся от него в контроллерах Spring

Всем привет! Проверяя задания в учебном центре моей компании, обнаружил, что двумя словами описать то, как можно избавиться от ResponseEntity в контроллерах не получится, и необходимо написать целую статью. Для начала, немного введения.

ВАЖНО! Статья написана для новичков в программировании и Spring в часности, которые знакомы со Spring на базовом уровне.

Что такое ResponseEntity<>? Представим ситуацию — у нас есть интернет магазин. И, при примитивной реализации, мы переходим по продуктам, передавая его Id в качестве параметра@RequestParam. Например, наш код выглядит таким образом:

 @ResponseBody @GetMapping("/products") public Product getProduct(@RequestParam Long id)

При запросе через адресную строку браузера, вывод будет в виде JSON, таким:

Однако, если мы обратимся к продукту, который у нас отсутствует, например с то получим следующую картину:

Для пользователя, или даже для фронтендщика, будет абсолютно непонятно, что пошло не так и в чём проблема. Совершая тот же запрос через Postman, ситуация яснее не будет:

И вот тут мы переходим к ResponseEntity<>. Этот объект представляет собой оболочку для Java классов, благодаря которой мы в полной мере сможем реализовать RESTfull архитектуру. Суть использования сводится к тому, чтобы вместо прямого возвращаемого типа данных в контроллере, использовать оболочку ResponseEntity<> и возвращать конечному пользователю, или, что скорее всего вероятно — фронту, JSON, который бы более-менее подробно описывал ошибку. Выглядит такой код примерно так:

 @GetMapping("/products") public ResponseEntity getProductRe(Long id)< try < Product product = productsService.findById(id).orElseThrow(); return new ResponseEntity<>(product, HttpStatus.OK); > catch (Exception e)< return new ResponseEntity<>(HttpStatus.NOT_FOUND); > >

Что здесь происходит? Вместо строгого типа Product, мы ставим ResponseEntity, где под ? понимается любой Java объект. Конструктор ResponseEntity позволяет перегружать этот объект, добавляя в него не только наш возвращаемый тип, но и статус, чтобы фронтенд мог понимать, что именно пошло не так. Например, при корректном исполнении программы, передавая мы увидим просто успешно переданный объект Product с кодом 200, а вот в случае продукта с результат уже будет такой:

Всё ещё не красиво, но уже хотя бы понятно, что продукт не найден. Мы имеем статус код 404 и фронт уже как-то может с этим работать. Это здорово, но нам бы хотелось более конкретного описания ошибки и результата. Давайте, в таком случае, создадим новый класс:

public class AppError < private int statusCode; private String message; public int getStatusCode() < return statusCode; >public void setStatusCode(int statusCode) < this.statusCode = statusCode; >public String getMessage() < return message; >public void setMessage(String message) < this.message = message; >public AppError() < >public AppError(int statusCode, String message) < this.statusCode = statusCode; this.message = message; >>

Это будет вспомогательный класс. Его задача — принять наше сообщение и переслать его фронту вместе со статусом 404. Как мы это сделаем? Очень просто:

 @GetMapping("/products") public ResponseEntity getProduct(Long id)< try < Product product = productsService.findById(id).orElseThrow(); return new ResponseEntity<>(product, HttpStatus.OK); > catch (Exception e)< return new ResponseEntity<>(new AppError(HttpStatus.NOT_FOUND.value(), "Product with id " + id + " nor found"), HttpStatus.NOT_FOUND); > >

В этом примере, если мы ловим ошибку, просто отдаём в конструктор ResponseEntity наш кастомный объект и статус 404. Теперь, если мы попробуем получить продукт с то ответ будет таким:

Отлично! Этого мы и хотели. Стало понятно, в чём проблема. Фронтенд легко распарсит этот JSON и обработает наше сообщение. Однако, сам метод контроллера теперь выглядит не слишком красиво. Да и когда сталкиваешься с чужим кодом, любой из нас сразу хотел бы видеть тип объекта, который будет возвращаться, а не какой-то там ResponseEntity со знаком вопроса в скобочках. Тут мы и переходим к основному материалу статьи.

Как избавиться от ResponseEntity в сигнатуре метода контроллера, при этом сохранив информативность возвращаемой ошибки?

Для этого нам понадобиться глобальный обработчик ошибок, который нам любезно предоставляется в пакете со спрингом. Для начала, создадим какой-то свой кастомный Exception, в котором унаследуемся от RuntimeException:

public class ResourceNotFoundException extends RuntimeException < public ResourceNotFoundException(String message) < super(message); >>

Здесь ничего особенного. Интересное начинается дальше. Давайте внимательно посмотрим на листинг этого класса:

@ControllerAdvice @Slf4j public class GlobalExceptionHandler < @ExceptionHandler public ResponseEntitycatchResourceNotFoundException(ResourceNotFoundException e) < log.error(e.getMessage(), e); return new ResponseEntity<>(new AppError(HttpStatus.NOT_FOUND.value(), e.getMessage()), HttpStatus.NOT_FOUND); > >

Начнём сверху вниз. @ControllerAdvice используется для глобальной обработки ошибок в приложении Spring. То есть любой Exception, выпадающий в нашем приложении, будет замечен нашим ControllerAdvice. @Slf4j используется для логгирования, заострять внимание мы на этом не будем. Далее создаём собственный класс, назвать его можем как угодно. И вот тут уже интересное — аннотация@ExceptionHandlerнад методом. Эта аннотация позволяет нам указать, что мы хотим перехватывать и обрабатывать исключения определённого типа, если они возникают, и зашивать их в ResponseEntity, чтобы вернуть ответ нашему фронту. В аргументах метода указываем, какую именно ошибку мы собираемся ловить. В данном случае, это наш кастомный ResourceNotFoundException. И возвращать мы будем точно такой же ResponseEntity, как и в примере выше, однако прописываем мы его уже всего 1 раз — в этом классе. Спринг на этапе обработки этой ошибки самостоятельно поймёт, что в методе нашего контроллера вместо нашего класса Product нужно будет вернуть ResponseEntity.

Теперь мы можем убрать из контроллера все ResponseEntity:

 @GetMapping("/products") public Product getProduct(Long id)

А логику появления ошибки перенести в сервисный слой:

 public Product findById(Long id) < return productsRepository.findById(id).orElseThrow( () ->new ResourceNotFoundException("Product with id " + id + " not found")); >

Теперь, если продукт не будет найден, выбросится ResourceNotFoundException. Наш глобальный обработчик исключений поймает это исключение, самостоятельно преобразует его в ResponseEntity и вместо Product’a вернут JSON с подробным описанием ошибки, как и прежде:

Таким образом, мы избавились от ResponseEntity и кучи лишнего кода, переписываемого из метода в метод, при этом сохранив всю функциональность, которую нам предоставляет ResponseEntity.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *