Автор оригинала: Hantsy Bai.
Несколько дней назад я создал пост для описания поддержки реактивного программирования в Spring 5 и его подпроектах, все коды этой статьи были обновлены до последней ВЕРСИИ Spring 5, проверьте spring-reactive-sample в моей учетной записи Github .
В этом посте я создам простую систему блога, включающую:
- Пользователь может входить и выходить из системы.
- Аутентифицированный пользователь может создать публикацию.
- Аутентифицированный пользователь может обновить публикацию.
- Удалить публикацию может только пользователь с ролью АДМИНИСТРАТОРА.
- Все пользователи(включая анонимных пользователей) могут просматривать список записей и сведения о публикациях.
- Аутентифицированный пользователь может добавлять свои комментарии к определенному сообщению.
Серверная часть будет построена с использованием новейшего реактивного стека Spring 5, включая:
- Весенняя загрузка 2.0, на данный момент последней версией является 2.0.0.M7
- Spring Data MongoDB поддерживает реактивные операции для MongoDB
- Весенняя сессия добавляет реактивную поддержку для
Веб-сессии
- Пружинная защита 5 выравнивается с пружинным 5 реактивным стеком
Передняя часть представляет собой СПА-центр на основе Angular, и он будет создан с помощью интерфейса командной строки Angular.
Исходный код размещен на Github, проверьте здесь .
Интерфейсные интерфейсы API
Предпосылки
Убедитесь, что вы уже установили следующее программное обеспечение.
- Oracle Java 8 SDK
- Апач Мавен
- Грейдл
- Ваша любимая среда разработки, в том числе:
- Интегрированная среда разработки NetBeans
- Среда разработки Eclipse IDE (или IDE на основе Eclipse, Настоятельно рекомендуется использовать пружинный набор инструментов)
- ИДЕЯ Intellij
Создайте скелет проекта
Самый быстрый подход для запуска загрузки на основе Spring-это использование Spring initializr .
Откройте браузер и перейдите к http://start.spring.io .
Выберите следующий стек:
- Используйте значение по умолчанию Maven как строительный инструмент, и Java как язык программирования
- Установить Пружинная загрузка версия до 2.0.0.M7
- В поле зависимости поиска выполните поиск реактивный , выберите Реактивный веб , Реактивный MongoDB в результатах поиска, а затем добавьте Безопасность , Сессия , Ломбок таким же образом и т.д.
Подсказка Создать проект кнопка или нажмите ALT+ENTER клавиши для загрузки сгенерированных кодов.
Извлеките файлы в локальную систему и импортируйте их в свою любимую среду разработки.
Создает API-интерфейсы RESTful
Давайте начнем с приготовления Post
API, ожидаемые API перечислены ниже.
/сообщения | ПОЛУЧИТЬ | [{идентификатор:’1′, название:’название’}, {идентификатор:’2′, название:’название 2′}] | Получить все сообщения |
/сообщения | СООБЩЕНИЕ {заголовок:’заголовок’,содержание:’содержание’} | {идентификатор:’1′, название:’название’,содержание:’содержание’} | Создайте новую запись |
/сообщения/{идентификатор } | ПОЛУЧИТЬ | {идентификатор:’1′, название:’название’,содержание:’содержание’} | Получить сообщение по идентификатору |
/сообщения/{идентификатор } | ПОМЕСТИТЕ {заголовок:’заголовок’,содержимое:’содержимое’} | {идентификатор:’1′, название:’название’,содержание:’содержание’} | Обновить конкретную публикацию по идентификатору |
/сообщения/{идентификатор } | УДАЛИТЬ | никакого контента | Удалить запись по идентификатору |
Создайте Сообщение
Класс POJO. Добавьте к этому классу специальную @Document
аннотацию Spring Data MongoDB, чтобы указать, что это документ MongoDB.
@Document @Data @ToString @Builder @NoArgsConstructor @AllArgsConstructor class Post implements Serializable { @Id private String id; @NotBlank private String title; @NotBlank private String content; }
Для удаления геттеров и сеттеров, toString
, равно
, Хэш-код
методы из Post
и чтобы исходный код выглядел четко, давайте использовать специальные аннотации Lombok для создания этих необходимых средств во время компиляции с помощью поддержки инструментов обработки аннотаций.
Создайте пострепозиционный
интерфейс для Сообщение
документ. Убедитесь, что он расширен из Репозитория реактивного Монго
, который является реактивным вариантом интерфейса MongoRepository
и он готов к реактивным операциям.
interface PostRepository extends ReactiveMongoRepository{ }
Давайте создадим класс Post Controller
для предоставления API RESTful.
@RestController() @RequestMapping(value = "/posts") class PostController { private final PostRepository posts; public PostController(PostRepository posts) { this.posts = posts; } @GetMapping("") public Fluxall() { return this.posts.findAll(); } @PostMapping("") public Mono create(@RequestBody Post post) { return this.posts.save(post); } @GetMapping("/{id}") public Mono get(@PathVariable("id") String id) { return this.posts.findById(id); } @PutMapping("/{id}") public Mono update(@PathVariable("id") String id, @RequestBody Post post) { return this.posts.findById(id) .map(p -> { p.setTitle(post.getTitle()); p.setContent(post.getContent()); return p; }) .flatMap(p -> this.posts.save(p)); } @DeleteMapping("/{id}") @ResponseStatus(NO_CONTENT) public Mono delete(@PathVariable("id") String id) { return this.posts.deleteById(id); } }
Это очень похоже на традиционный сервлет на основе @RestController
, разница в том, что здесь мы используем специфичный для реактора Моно
и Поток
в качестве возвращаемого типа результата.
ЗАПИСКА: Spring 5 также предоставляет возможности функционального программирования, проверьте spring-reactive-sample для получения более подробной информации.
Создайте компонент CommandLineRunner
, чтобы вставить некоторые фиктивные данные при запуске приложения.
@Component @Slf4j class DataInitializer implements CommandLineRunner { private final PostRepository posts; public DataInitializer(PostRepository posts) { this.posts = posts; } @Override public void run(String[] args) { log.info("start data initialization ..."); this.posts .deleteAll() .thenMany( Flux .just("Post one", "Post two") .flatMap( title -> this.posts.save(Post.builder().title(title).content("content of " + title).build()) ) ) .log() .subscribe( null, null, () -> log.info("done initialization...") ); } }
Теперь мы почти готовы запустить приложение. Но перед этим вы должны убедиться, что есть запущенный экземпляр MongoDB.
Я подготовил docker-compose.yml в корневой папке, используйте ее, мы можем быстро загрузить экземпляр MongoDB в контейнер Docker.
Откройте свой терминал и перейдите в корневую папку этого проекта, выполните следующую команду, чтобы запустить службу MongoDB.
docker-compose up mongodb
ПРИМЕЧАНИЕ. Вместо этого вы также можете установить сервер MongoDB в своей локальной системе.
Теперь попробуйте выполнить mvn spring-boot:выполнить
для запуска приложения.
Или нажмите Выполнить действие в контекстном меню проекта в вашей интегрированной среде разработки.
Когда он запустится, откройте другое окно терминала, используйте curl
или httpie
чтобы пройти тест.
curl http://localhost:8080/posts
Вы должны увидеть какой-то результат, подобный этому.
curl http://localhost:8080/posts [{"id":"5a584469a5f5c7261cb548e2","title":"Post two","content":"content of Post two"},{"id":"5a584469a5f5c7261cb548e1","title":"Post one","content":"content of Post one"}]
Отлично! это работает.
Далее давайте добавим правила контроля доступа для защиты API-интерфейсов Post
.
Защита API-интерфейсов
Как и традиционный MVC на основе сервлетов, когда spring-security-web
существует в пути к классам приложения web flux, Spring Boot автоматически настроит безопасность.
Чтобы настроить конфигурацию безопасности, создайте отдельный класс @Configuration
.
@Configuration class SecurityConfig { @Bean SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception { //@formatter:off return http .csrf().disable() .and() .authorizeExchange() .pathMatchers(HttpMethod.GET, "/posts/**").permitAll() .pathMatchers(HttpMethod.DELETE, "/posts/**").hasRole("ADMIN") .pathMatchers("/posts/**").authenticated() .pathMatchers("/user").authenticated() .pathMatchers("/users/{user}/**").access(this::currentUserMatchesPath) .anyExchange().permitAll() .and() .build(); //@formatter:on } private MonocurrentUserMatchesPath(Mono authentication, AuthorizationContext context) { return authentication .map(a -> context.getVariables().get("user").equals(a.getName())) .map(AuthorizationDecision::new); } @Bean public ReactiveUserDetailsService userDetailsService(UserRepository users) { return (username) -> users.findByUsername(username) .map(u -> User.withUsername(u.getUsername()) .password(u.getPassword()) .authorities(u.getRoles().toArray(new String[0])) .accountExpired(!u.isActive()) .credentialsExpired(!u.isActive()) .disabled(!u.isActive()) .accountLocked(!u.isActive()) .build() ); } }
В этой конфигурации мы определили Цепочку веб-фильтров безопасности
компонент для изменения правил сопоставления путей безопасности по умолчанию, как и ожидалось. И мы должны объявить Реактивный компонент UserDetailsService
, чтобы настроить UserDetailsService
, например. выборка пользователей из нашего MongoDB.
Создайте необходимый Пользовательский
документ и Пользовательский
интерфейс.
@Data @ToString @Builder @NoArgsConstructor @AllArgsConstructor @Document class User { @Id private String id; private String username; @JsonIgnore private String password; @Email private String email; @Builder.Default() private boolean active = true; @Builder.Default() private Listroles = new ArrayList<>(); }
В UserRepository
добавьте новый findByUsername
метод для запроса пользователя по имени пользователя , он возвращает Моно<Пользователь>
.
public interface UserRepository extends ReactiveMongoRepository{ Mono findByUsername(String username); }
Теперь давайте займемся аутентификацией.
В реальном мире для защиты API REST в основном используется аутентификация на основе токенов.
Весенняя сессия предоставляет простую стратегию для предоставления идентификатора сеанса в заголовках http-ответов и проверки правильности идентификатора сеанса в заголовках http-запросов.
В настоящее время Весенняя сессия обеспечивает реактивную поддержку Redis и MongoDB. В этом проекте мы используем MongoDB в качестве примера.
Добавьте spring-session-data-mongodb
в путь к классам проекта.
org.springframework.session spring-session-data-mongodb
Чтобы принудительно использовать БАЗОВУЮ аутентификацию HTTP для управления сеансами с помощью MongoDB, добавьте следующую конфигурацию в SecurityConfig
.
http .csrf().disable() .httpBasic().securityContextRepository(new WebSessionServerSecurityContextRepository()) .and()
Объявить Распознаватель идентификаторов веб-сеанса
компонент для использования HTTP-заголовка для разрешения идентификатора сеанса вместо файла cookie.
@Configuration @EnableMongoWebSession class SessionConfig { @Bean public WebSessionIdResolver webSessionIdResolver() { HeaderWebSessionIdResolver resolver = new HeaderWebSessionIdResolver(); resolver.setHeaderName("X-AUTH-TOKEN"); return resolver; } }
Предоставьте доступ к текущему пользователю с помощью API REST.
@GetMapping("/user") public Mono
Когда пользователь аутентифицирован, информация о пользователе может быть получена от введенного Участника
.
Добавьте начальных пользователей в Инициализатор данных
компонент.
this.users .deleteAll() .thenMany( Flux .just("user", "admin") .flatMap( username -> { Listroles = "user".equals(username) ? Arrays.asList("ROLE_USER") : Arrays.asList("ROLE_USER", "ROLE_ADMIN"); User user = User.builder() .roles(roles) .username(username) .password(passwordEncoder.encode("password")) .email(username + "@example.com") .build(); return this.users.save(user); } ) ) .log() .subscribe( null, null, () -> log.info("done users initialization...") );
Перезапустите приложение и попробуйте.
curl -v http://localhost:8080/auth/user -u user:password * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) * Server auth using Basic with user 'user' > GET /auth/user HTTP/1.1 > Host: localhost:8080 > Authorization: Basic dXNlcjpwYXNzd29yZA== > User-Agent: curl/7.57.0 > Accept: */* > < HTTP/1.1 200 OK < transfer-encoding: chunked < Content-Type: application/json;charset=UTF-8 < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < X-Content-Type-Options: nosniff < X-Frame-Options: DENY < X-XSS-Protection: 1 ; mode=block < X-AUTH-TOKEN: 39af0166-0f5f-4a1a-a955-21340b0b31b1 < {"roles":["ROLE_USER"],"name":"user"}* Connection #0 to host localhost left intact
Как вы видите, существует X-AUTH-ТОКЕН
заголовок в заголовках ответов при успешной аутентификации.
Попробуйте добавить X-AUTH-ТОКЕН
в заголовки HTTP-запросов и получить доступ к защищенным API.
curl -v http://localhost:8080/auth/user -H "X-AUTH-TOKEN: 39af0166-0f5f-4a1a-a955-21340b0b31b1" * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) > GET /auth/user HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.57.0 > Accept: */* > X-AUTH-TOKEN: 39af0166-0f5f-4a1a-a955-21340b0b31b1 > < HTTP/1.1 200 OK < transfer-encoding: chunked < Content-Type: application/json;charset=UTF-8 < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < X-Content-Type-Options: nosniff < X-Frame-Options: DENY < X-XSS-Protection: 1 ; mode=block < {"roles":["ROLE_USER"],"name":"user"}* Connection #0 to host localhost left intact
Да, это работает, как и ожидалось. Вы можете попробовать другие примеры на другом защищенном пути, такие как создание и обновление записей.
Обработка исключений
Для традиционного стека сервлетов мы можем использовать @ExceptionHandler
для обработки исключений и преобразования их в дружественные HTTP-сообщения.
В приложении на основе веб-потока мы можем объявить Обработчик веб-исключений
компонент для достижения этой цели.
Например, если по идентификатору не найдено сообщений, создайте исключение PostNotFoundException
и , наконец, преобразуйте его в ошибку 404 для HTTP-клиента.
Объявить Исключение Postnotfoundexception
как исключение времени выполнения
.
public class PostNotFoundException extends RuntimeException { public PostNotFoundException(String id) { super("Post:" + id +" is not found."); } }
Создайте Исключение PostNotFoundException
, когда оно не найдено, например. метод get
в Postscontroller
.
@GetMapping("/{id}") public Monoget(@PathVariable("id") String id) { return this.posts.findById(id).switchIfEmpty(Mono.error(new PostNotFoundException(id))); }
Объявите Обработчик веб-исключений
компонент для обработки Исключение PostNotFoundException
.
@Component @Order(-2) @Slf4j public class RestExceptionHandler implements WebExceptionHandler { private ObjectMapper objectMapper; public RestExceptionHandler(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public Monohandle(ServerWebExchange exchange, Throwable ex) { if (ex instanceof PostNotFoundException) { exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND); // marks the response as complete and forbids writing to it return exchange.getResponse().setComplete(); } return Mono.error(ex); } }
Для проверки компонентов мы можем преобразовать исключение Web Exchange BindException
в Необработанную сущность
ошибку клиента, пожалуйста, ознакомьтесь с полными кодами RestExceptionHandler
для получения более подробной информации.
В бэкенд-кодах я добавил некоторые функции, упомянутые в начале этого поста, такие как конечные точки комментариев, а также попытался добавить разбивку на страницы и функция аудита данных(когда она будет готова).
Далее давайте попробуем создать простое угловое интерфейсное приложение, чтобы обменяться рукопожатием с интерфейсными API.
Клиент
Предпосылки
Убедитесь, что вы уже установили следующее программное обеспечение.
- Последняя версия NodeJS , на данный момент я использовал NodeJS 9.4.0.
- Ваши любимые редакторы кода, VS Code, редактор Atom, Intellij WebStorm и т.д.
Установите интерфейс командной строки Angular по всему миру.
npm install -g @angular/cli
Убедитесь, что он установлен правильно.
> ng -v _ _ ____ _ ___ / \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _| / △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | | / ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | | /_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___| |___/ Angular CLI: 1.6.3 Node: 9.4.0 OS: win32 x64 Angular:
Подготовка скелета клиентского проекта
Откройте свой терминал, выполните следующую команду для создания углового проекта.
ng new client
Установите Угловой материал, Угловой Гибкий макет от Угловой команды.
npm install --save @angular/material @angular/cdk @angular/flex-layout
Некоторые модули материалов зависят от @angular/анимации
. Импорт Модуль анимации браузера
в Модуль приложения
.
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; @NgModule({ ... imports: [BrowserAnimationsModule], ... }) export class AppModule { }
Откройте polyfills.ts , раскомментируйте следующую строку,
import 'web-animations-js';
Затем установите веб-анимацию polyfill.
npm install --save web-animations-js
Чтобы включить жест в угловом материале, установите hammer js
.
npm install --save hammerjs
Импортируйте его в polyfills.ts .
import 'hammerjs';
Создайте структуру проекта
Следуйте за официальным Руководство по угловому стилю , использовать ng
команда для создания ожидаемых модулей и компонентов. Мы обогатим их позже.
ng g module home --routing=true ng g module auth --routing=true ng g module post --routing=true ng g module user --routing=true ng g module core ng g module shared ng g c home --module home ng g c post/post-list --module post ng g c post/post-form --module post ng g c post/new-post --module post ng g c post/edit-post --module post ng g c post/post-details --module post ng g c user/profile --module user ng g c auth/signin --module auth
Чтобы упростить работу с кодированием, я переношу свою бывшую работу Angular 2.x в этот проект, подробнее о шагах разработки Angular, ознакомьтесь с вики-страницами .
Angular 4.x представил новый Клиентский модуль Http
(находится в @angular/common/http
) вместо исходного @angular/http
модуля. Я обновил исходные коды для использования Клиентский модуль Http
для взаимодействия с API-интерфейсами REST в этом проекте.
Взаимодействие API-интерфейсов REST с модулем HttpClient
Давайте взглянем на обновленную Почтовую службу
. Здесь мы используем новые HttpClient
для замены устаревшего Http
, использование HttpClient
аналогично Http
, наибольшее отличие заключается в том, что его методы по умолчанию возвращают Наблюдаемый
.
const apiUrl = environment.baseApiUrl + '/posts'; @Injectable() export class PostService { constructor(private http: HttpClient) { } getPosts(term?: any): Observable{ const params: HttpParams = new HttpParams(); Object.keys(term).map(key => { if (term[key]) { params.set(key, term[key]); } }); return this.http.get(`${apiUrl}`, { params }); } getPost(id: string): Observable { return this.http.get(`${apiUrl}/${id}`); } savePost(data: Post) { console.log('saving post:' + data); return this.http.post(`${apiUrl}`, data); } updatePost(id: string, data: Post) { console.log('updating post:' + data); return this.http.put(`${apiUrl}/${id}`, data); } deletePost(id: string) { console.log('delete post by id:' + id); return this.http.delete(`${apiUrl}/${id}`); } saveComment(id: string, data: Comment) { return this.http.post(`${apiUrl}/${id}/comments`, data); } getCommentsOfPost(id: string): Observable { return this.http.get(`${apiUrl}/${id}/comments`); } }
Еще одна потрясающая функция клиентского модуля Http
добавлен ли долгожданный Http-перехватчик
официально.
Нам не нужно @covalent/http
для получения поддержки перехватчиков сейчас.
const TOKEN_HEADER_KEY = 'X-AUTH-TOKEN'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private token: TokenStorage, private router: Router) { } intercept(req: HttpRequest, next: HttpHandler): Observable | HttpUserEvent > { if (this.token.get()) { console.log('set token in header ::' + this.token.get()); req.headers.set(TOKEN_HEADER_KEY, this.token.get()); } return next.handle(req).do( (event: HttpEvent ) => { if (event instanceof HttpResponse) { const token = event.headers.get(TOKEN_HEADER_KEY); if (token) { console.log('saving token ::' + token); this.token.save(token); } } }, (err: any) => { if (err instanceof HttpErrorResponse) { console.log(err); console.log('req url :: ' + req.url); if (!req.url.endsWith('/auth/user') && err.status === 401) { this.router.navigate(['', 'auth', 'signin']); } } } ); } }
Сравните перехватчик HTTP, предоставленный в @covalent/http
, официальный Перехватчик Http
является более гибким и использует концепцию промежуточного программного обеспечения.
Исходные коды
Проверьте полные исходные коды из моего github.
Оригинал: “https://www.codementor.io/@hantsy/build-a-reactive-application-with-angular-5-and-spring-boot-2-0-fv8uif7wg”