Переписываем домашний проект на микросервисы (Java, Spring Boot, Gradle)

Введение


Image


Последние годы стала очень популярна тема микросервисов. Я не попадал на проекты с микросервисами, поэтому мне, естественно, захотелось ближе познакомиться с такой концепцией архитектуры.


Ранее у меня был домашний проект (хотя скорее даже его прототип), который было решено переписать на микросервисы. Проект представлял собой попытку сделать обучающую Java игру. То есть у игрока есть поле, на этом поле он может управлять каким-то юнитом с помощью кода. Пишет код, отправляет на сервер, там он выполняется и возвращает результат, который отображается пользователю.


Всё это было реализовано в виде прототипа — были пользователи, один урок и одна задача для него, возможность отправить код, который компилировался и исполнялся. Кое-какой фронтенд, но в статье о нём речи не будет. Технологии — Spring Boot, Spring Data, Gradle.


В статье будет реализован такой же прототип, но уже на микросервисах. Реализация будет наиболее простым путём (точнее наиболее простым, из известных мне). Реализация будет доступна любому, кто знаком со Spring.


В процессе изучения информации я нашёл хорошую статью, где аналогично разбивали некий небольшой монолит на микросервисы. Но там всё делалось на основе Spring Cloud, что безусловно правильнее, но мне хотелось сначала написать велосипед, чтобы потом на практике понимать от каких проблем лечат данные решения. Из данной статьи я использовал только Zuul.


Микросервисы


О том, что такое микросервисы писали много раз, например тут. Вкратце: вместо одного большого приложения, у нас много маленьких, у которых очень узкая область ответственности и которые общаются друг с другом.


Первый этап, это нужно разбить логику на ряд микросервисов:


  • user-service: сервис с пользователями (создание, просмотр, возможно авторизация)
  • lesson-service: сервис с уроками (создание, просмотр уроков и задач)
  • result-service: сервис с ответами (отправка выполненных задач, хранение результатов)
  • task-executor-service: сервис исполнения кода (компиляция и исполнение задач)

На этом этапе появляется мысль, что со всем этим зоопарком нужно как-то общаться фронтенду и отдельным микросервисам. Кажется неудобным, если все будут знать API и адреса друг друга.
Отсюда появляется ещё один сервис — gateway-service — общая точка входа.


Схема проекта будет выглядеть так:


diagram


Gateway-service


Поскольку я иду по самому простому пути, первой мыслью было сделать просто по контроллеру для каждого микросервиса, в которых будет перенаправление всех запросов по нужным адресам с помощью RestTemplate. Но, немного погуглив, я нашёл Zuul. У него есть интеграция со Spring Boot и конфигурация выходит крайне простой.


build.gradle сервиса выглядит так:


plugins {
    id 'java'
    id 'war'
}

apply plugin: 'spring-boot'

springBoot {
    mainClass 'gateway.App'
}

dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-zuul:1.2.0.RELEASE')
    compile('org.springframework.boot:spring-boot-starter-web')
}

А весь код микросервиса состоит из одного класса, App.java:


@SpringBootApplication
@EnableZuulProxy
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

Это включает ZuulProxy. Маршрутизация описывается в конфиге, у меня это application.properties:


zuul.routes.lesson-service.url=http://localhost:8081
zuul.routes.user-service.url=http://localhost:8082
zuul.routes.task-executor-service.url=http://localhost:8083
zuul.routes.result-service.url=http://localhost:8084

zuul.prefix=/services

Таким образом, запросы на /services/lesson-service/... будут направляться на http://localhost:8081/... и т.д. Получается очень удобное и простое решение для точки входа.
Zuul имеет много других различных фич типа фильтров, но нам от него больше ничего и не нужно.


Фронтенд так же, как мне кажется, в нашем случае должен отдаваться клиенту отсюда. Кладём всё что нужно в gateway-service/src/main/webapp/... и всё.


Остальные сервисы


Остальные сервисы будут сильно похожи друг на друга и их реализация мало чем отличается от привычного подхода. Но тут есть несколько моментов:


  1. Базы данных.
  2. Взаимодействие между микросервисами.

Базы данных


Каждый микросервис теперь может сам решать как ему хранить данные. Традиционным преимуществом микросервисов является как раз свобода выбора технологий независимо от других частей приложения.


Можно для каждого использовать новый тип базы данных. Но у меня просто появились три MySQL базы данных вместо одной, для user-service, lesson-service и answer-service. A task-executor-service должен хранить некий код задач, в который вставляется пользовательский код для выполнения задачи. Это будет храниться без БД, просто в виде файлов.


В момент разделения схемы на три базы у меня с непривычки возник вопрос — а как же внешние ключи, целостность данных на уровне бд и всё такое. Как оказалось никак. Точнее — всё на уровне бизнес-логики.


Взаимодействие между микросервисами


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


Создаём новый модуль в проекте, назовём его service-client. В нём будут, во-первых, классы для взаимодействия с сервисами, во-вторых общие классы для передачи данных. То есть у каждого сервиса есть свои какие-то Entity, соответствующие внутренней логике или схеме БД, но наружу они должны отдавать только экземпляры объектов из общей библиотеки.


Для классов-клиентов пишем абстрактный класс Client:


abstract class Client {
    private final RestTemplate rest;
    private final String serviceFullPath;

    private final static String GATEWAY_PATH = "http://localhost:8080/services";

    Client(final String servicePath) {
        this.rest = new RestTemplate(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
        this.serviceFullPath = GATEWAY_PATH + servicePath;
    }

    protected <T extends Result> T get(final String path, final Class<T> type) {
        return rest.getForObject(serviceFullPath + path, type);
    }

    protected <T extends Result, E> T post(final String path, final E object, final Class<T> type) {
        return rest.postForObject(serviceFullPath + path, object, type);
    }
}

GATEWAY_PATH — лучше задавать из конфига или ещё как-то, а не хардкодить в этом классе.
И пример наследования этого класса для lesson-service:


public class TaskClient extends Client {
    private static final String SERVICE_PATH = "/lesson-service/task/";

    public TaskClient() {
        super(SERVICE_PATH);
    }

    public Task get(final Long id) {
        return get(id.toString(), TaskResult.class).getData();
    }

    public List<Task> getList() {
        return get("", TaskListResult.class).getData();
    }

    public List<Task> getListByLesson(final Long lessonId) {
        return get("/getByLesson/" + lessonId, TaskListResult.class).getData();
    }

    public Task add(final TaskCreation taskCreation) {
        return post( "/add", taskCreation, TaskResult.class).getData();
    }
}

Тут может возникнуть вопрос, что за Result и почему мы возвращаем результат от getData() для него. Каждый контроллер возвращает не просто некий запрашиваемый объект в json, а ещё и дополнительную мета-информацию, которая в дальнейшем может быть полезна, поэтому при переписывании в микросервисы я не убрал это. То есть возвращается объект класса Result<T>, где T это сам запрашиваемый объект:


@Data
public class Result<T> {
    public String message;
    public T data;

    public static <T> Result<T> success(final T data) {
        return new Result<>(null, data);
    }

    public static <T> Result<T> error(final String message) {
        return new Result<>(message, null);
    }

    public static <T> Result<T> run(final Supplier<T> function ) {
        final T result = function.get();
        return Result.success(result);
    }
}

Тут нет метода getData(), хотя ранее в коде он используется. Всё это благодаря аннотации @Data от lombok, который я активно использовал.Result удобен тем, что далее можно легко добавлять некую мета-информацию (например время выполнения запроса), и как-то её использовать.


Теперь, чтобы использовать написанный нами код в других модулях, достаточно добавить зависимость (compile project(':service-client') в блок dependencies) и создать такой бин. Вот так выглядит конфигурация result-service:


@SpringBootApplication(scanBasePackages = "result")
@EnableJpaRepositories("result.repository")
@Configuration
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    public UserClient getUserClient() {
        return new UserClient();
    }

    @Bean
    public TaskClient getTaskClient() {
        return new TaskClient();
    }

    @Bean
    public ExecutorClient getTaskExecutor() {
        return new ExecutorClient();
    }
}

Его контроллер:


@RestController
@RequestMapping
public class ResultController {

    @Autowired
    private ResultService service;

    @RequestMapping(value = "/submit", method = RequestMethod.POST)
    public Result<TaskResult> submit(@RequestBody final SubmitRequest submit){
        return run(() -> service.submit(submit));
    }

    @RequestMapping(value = "/getByTask/{id}", method = RequestMethod.GET)
    public Result<List<AnswerEntity>> getByTask(@PathVariable final Long id) {
        return run(() -> service.getByTask(id));
    }
}

Видно, что везде контроллер возвращает некий Result<T>. И фрагмент сервиса:


@Service
@Transactional
public class ResultService {

    @Autowired
    private AnswerRepository answerRepository;

    @Autowired
    private TaskClient taskClient;

    @Autowired
    private ExecutorClient executorClient;

    public TaskResult submit(final SubmitRequest submit) {

        val task = taskClient.get(submit.getTaskId());
        if (task == null)
            throw new RuntimeException("Invalid task id");

        val result = executorClient.submit(submit);

        val answerEntity = new AnswerEntity();
        answerEntity.setAnswer(submit.getCode());
        answerEntity.setTaskId(task.getId());
        answerEntity.setUserId(1L);
        answerEntity.setCorrect(result.getStatus() == TaskResult.Status.SUCCESS);
        answerRepository.save(answerEntity);

        return result;
    }

    ...

answerEntity.setUserId(1L) — пока тут просто константа, ибо пока что совершенно непонятно как делать авторизацию.


В целом основную часть сделали, по образцу реализуем все остальные сервисы и всё должно работать. Но остаётся ещё разобраться с пользователями и их авторизацией. Это оказалось самой сложной частью для меня.


Авторизация


Раньше, до разбивки на микросервисы, авторизация была стандартная — по логину и паролю пользователь авторизировался в пределах контекста приложения.


Теперь задача расширяется, и каждый из сервисов должен понимать, авторизован ли пользователь, по которому пришёл к сервису запрос. И это при том, что запросы приходят не только непосредственно от пользователя, но и от других сервисов.


Первоначальный поиск привёл меня на разнообразные статьи, показывающие как шарить сессии с помощью Redis, но прочитанное мною показалось слишком сложным для hello-world домашнего проекта. Через некоторое время, вернувшись к вопросу, я уже нашёл информацию о JWT — JSON Web Token. Кстати, повторяя попытки поиска при написании этой статьи, я уже сразу натыкался на JWT.


Идея проста — вместо куков, на которых обычно держится авторизация, авторизирующий сервис будет выдавать некий токен, который в себя включает данные о пользователе, время выдачи и прочую нужную вам информацию. Затем, при любом обращении к сервисам, клиент должен передавать этот токен в заголовке (или как-либо ещё, как удобнее). Каждый сервис умеет его расшифровывать и понимать что это за пользователь, и ему не нужно лезть в базу и всё такое.


Тут возникает множество проблем, например как отзывать токен. Появляются идеи с несколькими токенами (длительного действия и короткого, второй используем для обычных запросов, первый для получения нового токена второго типа и как раз первый можно отозвать и для его проверки нужно лезть в БД).


На эту тему написано много статей, например эта, а также уже есть готовые библиотеки для использования.


Но у нас hello-world проект, поэтому нам не нужна серьёзная и совсем правильная авторизация, а нужно что-то, что можно быстро реализовать, но что тем не менее будет работать достаточно хорошо.


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


compile('org.springframework.boot:spring-boot-starter-security')
compile('io.jsonwebtoken:jjwt:0.7.0')

Второе необходимо как раз для генерации самого токена. Токен генерируем следующим образом на запрос с верным логином и паролем:


private String getToken(final UserEntity user) {
    final Map<String, Object> tokenData = new HashMap<>();
    tokenData.put(TokenData.ID.getValue(), user.getId());
    tokenData.put(TokenData.LOGIN.getValue(), user.getLogin());
    tokenData.put(TokenData.GROUP.getValue(), user.getGroup());
    tokenData.put(TokenData.CREATE_DATE.getValue(), new Date().getTime());
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.DATE, tokenDaysAlive);
    tokenData.put(TokenData.EXPIRATION_DATE.getValue(), calendar.getTime());
    JwtBuilder jwtBuilder = Jwts.builder();
    jwtBuilder.setExpiration(calendar.getTime());
    jwtBuilder.setClaims(tokenData);
    return jwtBuilder.signWith(SignatureAlgorithm.HS512, key).compact();
}

key тут это секретный ключ токена, который должны знать все сервисы для декодирования токена. Мне не понравилось, что его нужно писать в конфиг каждого сервиса, но другие варианты сложнее.


Далее нам нужно написать фильтр, который при каждом запросе будет проверять токен и авторизировать если всё ок. Но фильтр уже будет не в user-service, а в service-client, т.к. это общий код для всех сервисов.


Сам фильтр:


public class TokenAuthenticationFilter extends GenericFilterBean {

    private final TokenService tokenService;

    public TokenAuthenticationFilter(final TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        final String token = ((HttpServletRequest) request).getHeader(TokenData.TOKEN.getValue());
        if (token == null) {
            chain.doFilter(request, response);
            return;
        }

        final TokenAuthentication authentication = tokenService.parseAndCheckToken(token);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }
}

Если token не прислали — не делаем ничего, иначе пытаемся авторизировать клиента. Проверка авторизации осуществляется уже не нами, а дальше в другом (стандартном) фильтре от spring security. TokenService, где происходит непосредственная проверка токена:


public class TokenService {

    private String key;

    public void setKey(String key) {
        this.key = key;
    }

    public TokenAuthentication parseAndCheckToken(final String token) {
        DefaultClaims claims;
        try {
            claims = (DefaultClaims) Jwts.parser().setSigningKey(key).parse(token).getBody();
        } catch (Exception ex) {
            throw new AuthenticationServiceException("Token corrupted");
        }

        if (claims.get(TokenData.EXPIRATION_DATE.getValue(), Long.class) == null) {
            throw new AuthenticationServiceException("Invalid token");
        }

        Date expiredDate = new Date(claims.get(TokenData.EXPIRATION_DATE.getValue(), Long.class));
        if (!expiredDate.after(new Date())) {
            throw new AuthenticationServiceException("Token expired date error");
        }

        Long id = claims.get(TokenData.ID.getValue(), Number.class).longValue();
        String login = claims.get(TokenData.LOGIN.getValue(), String.class);
        String group = claims.get(TokenData.GROUP.getValue(), String.class);

        TokenUser user = new TokenUser(id, login, group);

        return new TokenAuthentication(token, true, user);
    }
}

TokenData это enum для удобства, из которого можно взять строковые представления полей. Ещё тут есть два класса — TokenUser (это класс с тремя полями) и TokenAuthentication:


public class TokenAuthentication implements Authentication {
    private String token;
    private Collection<? extends GrantedAuthority> authorities;
    private boolean isAuthenticated;
    private TokenUser principal;

    public TokenAuthentication(String token, boolean isAuthenticated,
                               TokenUser principal) {
        this.token = token;
        this.authorities = Collections.singletonList(new SimpleGrantedAuthority(principal.getGroup()));
        this.isAuthenticated = isAuthenticated;
        this.principal = principal;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getDetails() {
        return null;
    }

    @Override
    public String getName() {
        if (principal != null)
            return principal.getLogin();
        else
            return null;
    }

    @Override
    public Object getPrincipal() {
        return principal;
    }

    @Override
    public boolean isAuthenticated() {
        return isAuthenticated;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        this.isAuthenticated = isAuthenticated;
    }

    public String getToken() {
        return token;
    }

}

Конфиг user-service теперь будет выглядеть так:


@SpringBootApplication
@EnableJpaRepositories("user.repository")
@Configuration
@ComponentScan(value = "user")
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled=true)
public class App extends WebSecurityConfigurerAdapter {

    @Value("${token.key}")
    private String tokenKey;

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .headers().frameOptions().sameOrigin()
                .and()
                .csrf()
                .disable()
                .addFilterAfter(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean(name = "tokenAuthenticationFilter")
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter(tokenService());
    }

    @Bean(name = "tokenService")
    public TokenService tokenService() {
        TokenService tokenService = new TokenService();
        tokenService.setKey(tokenKey);
        return tokenService;
    }
}

Ключевое тут — .addFilterAfter(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class), регистрируем фильтр и указываем когда он должен запускаться. Ограничивать же доступ к ресурсам мне нравится не в конфиге, а аннотациями над методами контроллеров, например @Secured("ROLE_ADMIN").


Появление токена делает необходимым приём его не только от клиента, но и от других сервисов, соответственно нужно его уметь и отправлять дальше. Для этого я просто принимаю из заголовков токен в контроллерах, где он необходим, и передаю его в методы сервис-клиента. То есть у класса Client становится по два метода get и post, для случае с токеном и без него, пример:


protected <T extends Result> T get(final String path, final Class<T> type) {
    return rest.getForObject(serviceFullPath + path, type);
}

protected <T extends Result> T get(final String path, final Class<T> type, final String token) {
    HttpHeaders headers = new HttpHeaders();
    headers.set(TokenData.TOKEN.getValue(), token);
    HttpEntity entity = new HttpEntity(headers);
    return rest.exchange(serviceFullPath + path, HttpMethod.GET, entity, type).getBody();
}

Соответствующим образом меняются конкретные классы-клиенты. А в контроллере мы получаем токен с помощью аннотации, например:


@PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/submit", method = RequestMethod.POST)
public Result<TaskResult> submit(@RequestBody final SubmitRequest submit, @RequestHeader("token") String token){
    return run(() -> service.submit(submit, token));
}

Хоть это и простое решение, оно кажется неудобным — постоянная возня с токеном, его приём, передача в методы. Более правильным мне кажется было бы сделать всё тоже самое автоматическим.


При авторизации по токену запоминать его (это почти уже сделано, можно посмотреть на класс TokenAuthentication), а при использовании классов Client автоматически доставать токен, если он есть, и передавать в следующий сервис.


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


Запуск


Есть не один готовый продукт, позволяющий разворачивать и управлять микросервисами, следить за их состоянием и прочее, но мне кажется, что это уже выходит за пределы hello-world. Для запуска я просто выполняю bootRun для каждого сервиса и наслаждаюсь результатом.


Заключение


В целом это был интересный и полезный опыт (писать велосипеды всегда интересно), но если бы я хотел развивать свой проект дальше — я бы откатил все изменения и продолжил бы работу над классическим монолитом, т.к. на масштабах такого небольшого проекта сложность управления этим всем сильно возросла, а преимуществ мало.


Как уже говорили много раз другие люди в различных статьях — выбор такой архитектуры уместен не везде и должен приниматься здраво. Или же чисто ради самообучения писать такие велосипеды, как описанный выше.


Надеюсь, статья была полезна.


→ Полный исходный код проекта можно найти тут.

Метки:
Поделиться публикацией
Похожие публикации
Комментарии 26
  • 0

    Зачем использовать для микросервисов то, что для них не очень подходит?

    • 0
      А что для них не подходит в данном случае?
      • –2

        Spring boot уже научился работать без встроенного tomcat? Если нет, то неподходит tomcat, так как он из старого мира монолитных приложений и тащит много лишнего.

        • 0
          В теории Spring Boot можно использовать без встроенного tomcat, но на практике лично я не пробовал.
          Но в целом, Spring Boot вроде как очень широко используется для микросервисов.
          • –3

            Если судить по статьям, вроде как предлагают использовать другие фрейморвки со встроенными http серверами.


            Хотя, может я ошибаюсь, я давно не использовать Java и не знаю, как там сейчас дела(

          • +1
            Бут умеет работать с любым сервером, из коробки есть реализации для пары самых популярных. Эмбеддед-томкат ничего лишнего не тащит уже очень давно и разбит на модули. Так что спринг-бут это как раз идеальный инструмент для микросервисов, которые на нем можно поднять в несколько строк, если еще и со спринг клауд, то и большинство проблем описанных в статье решает, так что не надо сбивать людей.
            • 0
              Научился. При подключении зависимости отключаете tomcat, подключаете jetty или undertow — для них уже есть готовые артефакты.
              Собственно на текущем проекте планируем подключить spring boot вместо обычного spring(наследный код) в том числе и для того, чтобы уйти от tomcat'а.
              • 0
                Можете раскрыть мысль по поводу «в томкате много лишнего»?
                там по сути только сервер и контейнер сервелтов.
                По весу-сейчас зашел на сайт томката -зип-архив стэндэлон томката весит 9.5Мб. Это будет меньше чем некоторые либы.
                • –2

                  Томкат это почти Java EE и он поддерживает вроде неплохое число спецификаций (те же сервлеты). Как мне кажется, для микросервисов они излишни и только зря их нагружают.

                  • 0
                    Это не java ee и не так уж много он реализаций стандартов за собой тащит. Как я написал выше, есть возможность подключить только core, который реализует сервер+сервлеты. Сервлеты в свою очередь это тоже не бесполезная вещь, хоть и может быть излишней для мест, где особенно важна производительность, но в общем случае они дают достаточно тонкую абстракцию, где поток байт приходящих по хттп превращается в объект запроса и диспатчится в какой-то обработчик. Это всё равно обычно микросервису нужно. Так что ваши заявления похожи на откровенные домыслы и вообще джава тормозит.
                    • 0

                      Я не считаю, что Java тормозит, но мне определенно кажется, что вот такой вот workflow, с использованием нативных потоков немного устарел.


                      Я не прав?

                      • 0
                        В мир нанотехнологий не все используют нанопокрытия на молотках. Я не хочу сказать что томкат модный-молодежный, но он работает как часы, а выйгрыш в производительности в 2% для данного конкретного случая не даст ничего.

                        Если же вам кажется что томкат вам мал, вы всегда можете подключить любой другой сервер из поддерживаемых спринг бутом или даже написать свой адаптер или даже сервер.
                        • 0

                          Проблема микросервисной архитекруты в том, что один монолит заменяется на кучу мелких структур.


                          И если каждая структура инициализируется большим количеством излишеств, то потом возникают отбитые статьи в духе "Мы переписали микросервисы со Sprint+Tomcat на Golang и получил выграш по всем ресурсам в 10 раз, потому что мы поменяли архитектуру потому что Java шлак, а Golang рулит".

                          • 0
                            Имея некоторе желание и умение, можно на чем угодно написать приложение, которое будет в 10 раз медленнее, чем другое приложение делающее тоже самое. Только это не проблема спринга или томката, разруха как известно она в головах.
                        • 0
                          Пардон, а вы в свете современных веяней хотите в контексте духа святого запрос обрабатывать? Всё равно будет некоторый пул обработчиков, который будет делать собственно полезную работу. Конечно, бывает некоторая специфика, когда нам из io-треда надо отправить сразу байтики в другой асинхронный io и тогда вся эта архитектрура не очень походит, но для этих редких случаев никто и не навязывает использование томката. Можно использовать например netty-reactor или что-нибудь другое более подходящее для вашего решения. Для большинства же сервисов, когда нам нужно обработать рест запрос, пообщаться с базой и другими сервисами и ответить пользователю, вполне подойдет сервлет-апи, тем более, что начиная с 3 версии у сервлетов есть асинхронный api (и спринг это отлично поддерживает) который позволяет не занимать поток из пула, а утилизировать это время по-другому.
                      • –1
                        В проекте используется спринг с DI, я думаю он явно более тяжелый, чем томкат.
                        А сервлеты необходимы, поверх них работает спринг и по сути псе что бывает на аппликэйшн серверах
                    • 0
                      Можно вместо сервера по умолчанию использовать Jetty, например…
                      • 0
                        Spring Boot всегда можно было использовать без встроенного сервера. Кроме того, если вас не устраивает встроенный Tomcat, то его можно заменить на Undertow или Jetty.
                  • 0
                    Удивило, что клиент очень напомнил мне мой, недавно реализованный на C#
                    ApiClient
                    public class ApiClient
                    {
                    	protected readonly ApiContext _context;
                    	protected readonly string _apiPrefix;
                    
                    	public ApiClient(ApiContext context, string apiPrefix)
                    	{
                    		_context = context;
                    		_apiPrefix = apiPrefix;
                    	}
                    
                    	protected Task<ResponseT> GetAsync<ResponseT>(string method, string param = null)
                    	{
                    		return RestRequest.GetAsync<ResponseT>(
                    			RequestUrl( method, param ) + $"?accessToken={GetAccessToken()}"
                    		);
                    	}
                    
                    	protected Task<ResponseT> PutAsync<ResponseT>(string method, string param = null)
                    	{
                    		return RestRequest.PutAsync<ResponseT>(
                    			RequestUrl( method, param ),
                    			$"accessToken={GetAccessToken()}"
                    		);
                    	}
                    
                    	protected Task<ResponseT> PostAsync<ResponseT>(string method, Dictionary<string, string> fields)
                    	{
                    		return RestRequest.PostAsync<ResponseT>(
                    			RequestUrl( method ),
                    			fields
                    		);
                    	}
                    
                    	protected Task<ResponseT> DeleteAsync<ResponseT>(string method, string param = null)
                    	{
                    		return RestRequest.DeleteAsync<ResponseT>(
                    			RequestUrl( method, param ) + $"?accessToken={GetAccessToken()}"
                    		);
                    	}
                    
                    	protected string RequestUrl(string method, string param = null)
                    	{
                    		var url = $"{_context.apiServerUrl}{_apiPrefix}";
                    
                    		if (!string.IsNullOrEmpty( method ))
                    			url += "/" + method;
                    
                    		if (!string.IsNullOrEmpty( param ))
                    			url += "/" + param;
                    
                    		return url;
                    	}
                    
                    	protected string GetAccessToken()
                    	{
                    		if (!_context.isAuthorized)
                    			throw new Exception("Context ins't authorized");
                    
                    		return _context.accessToken;
                    	}
                    }
                    

                    • 0
                      Читается как
                      Помогите программисту J не добраться до виски.

                      Так специально задумано?
                      • 0
                        Вряд ли, скорее случайность
                      • 0
                        Spring Boot, микросервисы и без Spring Cloud?
                        • 0
                          Про авторизацию через токены — интересно было почитать, взял на заметку. Приходилось делать авторизацию только по логину-паролю.
                          По клиенту — можно посмотреть в сторону Feign, есть простое и красивое решение. Пример
                          • 0
                            спасибо, интересное решение
                          • 0
                            Так самый интересный вопрос — сколько времени ушло на реализацию этого hello world? Точнее, на переделку под микросервисы?
                            • 0

                              Сложно сказать, не засекал, но вроде как не сильно много, за исключением авторизации, с которой были проблемы. Сначала там смотрел на Redis сессии, потом уже JWT. Даже чисто по статье, на которую я ссылаюсь, сходу не вышло потому что AbstractAuthenticationProcessingFilter не подходит для моих целей.


                              Без авторизации статья вышла бы на пол года раньше, никак не мог найти время чтобы разобраться до конца.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.