Рубрики
Без рубрики

Создайте реактивное приложение с помощью Angular 5 и Spring Boot 2.0

Я создал сообщение (https://medium.com/@hantsy/reactive-programming-with-spring-5-3bfc5d324ba0) для описания поддержки реактивного программирования в Spring 5 и его подпроектах, все коды этого…

Автор оригинала: 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

Предпосылки

Убедитесь, что вы уже установили следующее программное обеспечение.

Создайте скелет проекта

Самый быстрый подход для запуска загрузки на основе 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 Flux all() {
        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 Mono currentUserMatchesPath(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 List roles = 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 current(@AuthenticationPrincipal Mono principal) {
    return principal
            .map( user -> {
                Map map = new HashMap<>();
                map.put("name", user.getName());
                map.put("roles", AuthorityUtils.authorityListToSet(((Authentication) user)
                        .getAuthorities()));
                return map;
            });
}

Когда пользователь аутентифицирован, информация о пользователе может быть получена от введенного Участника .

Добавьте начальных пользователей в Инициализатор данных компонент.

this.users
    .deleteAll()
    .thenMany(
        Flux
            .just("user", "admin")
            .flatMap(
                username -> {
                    List roles = "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 Mono get(@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 Mono handle(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”