Чувак, ну ты вообще бешеный.
А чем плохо-то? На спринге так и пишут. Многие, правда, из спринга знают только MVC-шные аннотации да DI, но чтобы не лезть в дикую императивщину, кто-то должен сделать за вас этот FizzBuzz.

Зачем это на Java писать? Есть гораздо лучшие средства для этого, например, Node.js.

точно лучше? если проект на java

Я, конечно, больше по питону, но уж лучше жаба, чем жабоскрипт :-)

з.ы. а если серьёзно — какие плюсы тут даст нода, которые не дадут другие средства? Единственное, что приходит в голову — асинхронщина, так её куда только не завезли
Лучше на Node.js?! Интересно чем же лучше?
Чем Java, естественно ))

привет от Армянского радио
Тонко)
Смысл от спринга, если все равно создаем сами синглтон?
Контейнер, который является синглтоном, можно скрыть от конечной целевой разработки
Сомнительное решение.
1. В методе TelegramUpdateHandlerBeanPostProcessor.postProcessAfterInitialization аргумент bean — это прокся, у которой на методах уже нет никаких аннотаций.
Решение: заменить обращения к bean на botControllerMap.get(beanName)
2. TelegramUpdateHandlerBeanPostProcessor вызывается в середине процесса настройки бинов. Сохраняемый «контроллер» не факт, что полностью настроен.
Решение: наполнять контейнер только после инициализации всего спринга (например, через ApplicationListener). Или сохранять не конкретный «контроллер», а его beanName, чтобы в момент обработки брать настроенный «контроллер» из контекста спринга.
3. Если «контроллер» будет иметь скоуп отличный от синглтона, то это решение тоже работать не будет, так как его создание будет происходить не при старте спринга, а когда-то.
Решение: наполнять контейнер на этапе BeanFactoryPostProcessor, сохраняя тройки path-beanName-method
1) Спасибо за замечание, поправлю. Хотя в данном примере АОП не используется и соответственно бины в прокси не оборачиваются
2) На самом деле для нашего случая в каком месте будут обрабатываться наши бины неважно.
Также я специально реализовал интерфейс Ordered, в методе которого я указал низкий приоритет, чтобы TelegramUpdateHandlerBeanPostProcessor обрабатывал бины в конце.
3) Наверно для данной целевой области не имеет смысла. Ну а решить эту таску всегда можно при необходимости.
Очень даже Spring- и Java- way. Я бы в дополнение к этому добавил еще новый Spring Scope для привязки бинов к пользователям и поддержки stateful диалогов.

SpringConfig.java
public class SpringConfig implements BeanFactoryPostProcessor {

    @Bean
    public UserScope userScope(ConfigurableListableBeanFactory beanFactory) {
        return new UserScope(beanFactory);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        final UserScope userScope = beanFactory.getBean(UserScope.class);
        beanFactory.registerScope(UserScope.SCOPE, userScope);
    }
}



UserScope.java
public class UserScope implements Scope {

    public static final String SCOPE = "user";
    private static final Logger logger = LoggerFactory.getLogger(UserScope.class);
    private static final ThreadLocal<User> USER = new ThreadLocal<>();

    private final Object lock = new Object();
    private final ConfigurableListableBeanFactory beanFactory;
    private final Cache<String, Map<String, Object>> conversations;

    public UserScope(ConfigurableListableBeanFactory beanFactory) {
        this.beanFactory = beanFactory;
        // По истечению 1 часа пользовательские бины удаляются
        conversations = CacheBuilder.newBuilder()
                .expireAfterAccess(1, TimeUnit.HOURS)
                .removalListener(notification -> {
                    if (notification.wasEvicted()) {
                        Map<String, Object> userScope = (Map<String, Object>) notification.getValue();
                        userScope.values().forEach(this::removeBean);
                    }
                })
                .build();
    }

    public static User getUser() {
        return USER.get();
    }

    public static void setUser(User user) {
        USER.set(user);
    }

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        final String userId = getConversationId();
        if (userId != null) {
            final String userName = MessageUtils.getSenderName(getUser());
            Map<String, Object> beans = conversations.getIfPresent(userId);
            if (beans == null) {
                synchronized (lock) {
                    beans = conversations.getIfPresent(userId);
                    if (beans == null) {
                        beans = new ConcurrentHashMap<>();
                        conversations.put(userId, beans);
                        logger.debug("Bean storage for user '{}' is initialized", userName);
                    }
                }
            }
            Object bean = beans.get(name);
            if (bean == null) {
                bean = objectFactory.getObject();
                beans.put(name, bean);
                logger.debug("Bean {} is created for user '{}'", bean, userName);
            }
            return bean;
        }
        //return null;
        throw new RuntimeException("There is no current user");
    }

    @Override
    public Object remove(String name) {
        final String userId = getConversationId();
        if (userId != null) {
            final Map<String, Object> userBeans = conversations.getIfPresent(userId);
            if (userBeans != null) {
                return userBeans.remove(name);
            }
        }
        return null;
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {

    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Override
    public String getConversationId() {
        final User user = getUser();
        return user == null ? null : user.getId().toString();
    }

    public void removeConversation() {
        final String userId = getConversationId();
        if (userId != null) {
            final String userName = MessageUtils.getSenderName(getUser());
            final Map<String, Object> beans = conversations.getIfPresent(userId);
            if (beans != null && !beans.isEmpty()) {
                beans.values().forEach(this::removeBean);
                synchronized (lock) {
                    conversations.invalidate(userId);
                    logger.debug("Bean storage for user '{}' is invalidated", userName);
                }
            }
        }
    }

    private void removeBean(Object bean) {
        try {
            beanFactory.destroyBean(bean);
        } catch (Exception ex) {
            logger.error("An error has occurred during destroying bean {}", bean, ex);
        }
    }
}



Т.е. в бинах с аннотацией Scope(UserScope.SCOPE) можно смело хранить состояние диалога с одним, конкретным пользователем в течение небольшого времени

Я не пишу на Java, но TelegramUpdateHandlerBeanPostProcessor это шутка такая?
Это набирать вообще реально? Или в Java это так и надо делать...

К сожалению путь к простому лежит через терни сложного

в нормальных IDE достаточно писать только заглавные буквы, чтобы тебе IDE предложила подставить полное название, поэтому нет заморочек на "надо имя класса покороче"

Меня единственного беспокоит вопрос сценариев? Каждый раз открываю статью про Telegram API и надеюсь, что в ней хоть капельку затронут систему сценариев.

На мой взгляд, бот, который получает одну команду и по ней сразу формирует ответ — это чрезмерно тривиальная задача. И пусть та же задача обернута здесь в зеленую упаковку Spring MVC, тем не менее, задача остается тривиальной, а бот примитивным.

Вопрос открыт: Как спроектировать гибкую систему сценариев?
Так, чтобы клиент выбрал сценарий (а программист без труда его составил), и ему предоставлялось целое дерево всевозможных вопросов, каждый из которых зависит от предыдущего и обладает полным контекстом ответов.
А в итоге, например, вызывается финальный метод, который программист должен переопределить, в котором будет получен финальный результат — ответы на все вопросы сценария.

У меня есть идеи на этот счет. Например, использование DSL от Groovy или Kotlin для описания сценариев, но вопрос все еще открыт.
Быть может, когда-нибудь закончу, да напишу статью.
А пока — было бы здорово выслушать мысли людей, подискутировать по действительно проблемной теме.
Как вариант, реализация может быть основана на конечных автоматах, например BPM-системе. Бот получает команды и запускает соответствующие процессы, Service Task-и которых выполняют необходимые действия и отвечают пользователю. Один диалог (считай пользователь) = один или несколько, связанных с ним, экземпляров бизнес-процессов.

Клиент рисует бизнес-процесс или выбирает из готовых, а программист закладывает логику в Service Task-и.
Идея годная, только реализация все-таки получается достаточно объемная. Ну, если вкладывать в платформу функционал BPM системы.
Плюс, если я правильно понимаю, для программиста не становится тривиальной задачей разработка сценария любой сложности. Да, BPM системы позволят заложить любой сценарий, но это не значит, что его реализация будет тривиальна.
Плюс я правильно понимаю, что программист будет вынужден реализовывать функционал для каждого шага?

Рассмотрим такой простой пример:
Я: Оповещения
Бот: О чем нужно оповещать?
Я: Курс валют
Бот: Утром, Днем, Ночью, Каждый час?
Я: Утром
Бот: В какое время? (9:00, 10:00, 11:00, 12:00)
Я: 9:00
Бот: Отлично, каждое утро в 9:00 я буду предоставлять тебе свежую информацию по курсам валют.

Когда бот спрашивает о времени (утро, день и пр.), на этом этапе либо заканчиваются вопросы, либо следующие зависят от ответа на текущий. Мало того, важно понимать: чаще всего клиент должен лишь ответить на определенные вопросы «анкеты» (заполнить форму) и получить результат по итогу ответов на все вопросы, но платформа не должна исключать возможности на любом шаге нарастить функционал программистом (например, отправить запрос).

Я пока рассматриваю простые варианты, при которых сценарии описывает программист. И описывает так, что ему это удобно. Удобно писать любой сложности сценарии, удобно их редактировать и отлаживать. Хотелось бы, чтобы один сценарий был в одном месте, скажем, в одном файле. И весь этот файл со сценарием был бы вполне самодостаточен.

Вот тут ребята описывали сценарий в XML:
image

На мой взгляд, идея годная, но с DSL от Groovy мне нравится гораздо больше.
Плюс я правильно понимаю, что программист будет вынужден реализовывать функционал для каждого шага?
Нет, необязательно. Достаточно реализовать прием команд и отправление ответов и переиспользовать этот функционал на каждом следующем шаге.

Ваш сценарий XML хорош, но для очень простой машины состояний. Очень скоро его перестанет хватать и придется дорабатывать. Этот XML можно легко заменить одним Java-классом, с enum-ом всех возможных состояний и методами для каждого состояния. Если пойти дальше, то каждый элемент enum может хранить в себе ссылку на соответствующий groovy-скрипт (либо на функцию внутри одного большого groovy-скрипта). Тогда Java-класс, получив очередную команду, вызывает groovy-скрипт, соответствующий текущему состоянию, который выполняет необходимые действия и, помимо всего прочего, переключает состояние на новое.

Посмотрите на этот BPMN-процесс (накидал за 5 минут на коленке)
image
Под капотом у него все тот же XML, вместо State-ов используются ServiceTask-и, Gateway-и и Event-ы. Вместо transition — incoming and outcoming SequenceFlow.
Task-и пишутся на Java, Groovy, JavaScript и всем том, что поддерживает Java Scripting API. Сам бизнес-процесс рисуется и тестируется в уютненькой среде.
Евгений, супер! Спасибо за комментарий! Очень годный! Жаль, проморгал, не успел лайкнуть.
Только полноправные пользователи могут оставлять комментарии.
Войдите, пожалуйста.