Ку, Хабр!
Немного откровения: каждый раз написав if с несколькими условиями или switch с полотном кейсов, чувствую, что где-то плачут котики. Чем больше условий, тем больше котиков. Рука тянется оптимизировать, переписать, переделать и накормить котиков.
Собственно, статья — индульгенция. Решение одного из кейсов при помощи рефлективной парадигмы программирования.
Работаю я в Beeline Кыргызстан, и у телекома часто на повестке дня кейсы с условием высокой нагрузки и масштабирования. Задача — реализовать application, принимающего неограниченное количество команд с обработкой в многопоточном режиме. Каждый раз писать еще один условный блок — решение так себе, как и вариант с регулярными выражениями. И оба варианта лишь добавят мотивации склонному к насилию психопату, который знает, где я живу, если этих команд 100 или 1000 например.
Если вы привыкли сразу читать код, то ссылка на репозиторий тут.
Идея следующая: двигаемся по пути паттерна Mediator (или Intermediary, Controller), создадим диспетчера, который будет определять назначение той или иной команды, и вызывать аннотированный метод с логикой.
Например, приходит команда /command/observeAllUserActions. По имени команды observeAllUserActions, определяем класс-обработчик и выполняем логику одного конкретного метода, который слушает например таблицу логов, фильтрует и возвращает все действия юзеров в один определенный топик. На фронте подписываемся на этот топик и видим данные в реалтайме.
Итак, начнем.
Пара слов об аннотациях для понимания. Аннотация — это по сути метка в коде, метаданные для определенного типа элементов (пакет, класс, метод, конструктор и т.д). Пометив код, в дальнейшем используем его в рантайме, определив по этой самой аннотации.
Наши две аннотации CommandHandler и HandlerMethod:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Component public @interface CommandHandler { }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface HandlerMethod { AppEventsEnum value(); }
RetentionPolicy — это жизненный цикл аннотации и у нас указан RUNTIME, еще есть CLASS и SOURCE.
CLASS указываем, если метаданные нужны только в исходном коде, SOURCE если в скомпилированном файле и RUNTIME если нужен в процессе выполнения кода.
Target — тип элемента, для которого создается аннотация, это может быть класс, метод, конструктор, поле и т.д.
Также добавим две зависимости
<dependency> <groupId>org.reflections</groupId> <artifactId>reflections</artifactId> <version>0.9.10</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
Библиотека Reflections работает как сканер CLASSPATH. Индексирует отсканированные метаданные и позволяет запрашивать их во время выполнения(в рантайме). Также умеет сохранять эту информацию, и можно использовать ее в любой момент проекта без необходимости повторного сканирования CLASSPATH.
Jackson Project в представлении не нуждается. Библиотека для парсинга/генерации JSON.
Описываем типы для будущих event handle классов:
@Data @NoArgsConstructor @AllArgsConstructor public class AppCommand { protected Map<String, String> arguments; }
Команда без аргументов
public class Command1 extends AppCommand { public Command1() { super(); } }
Имплементация для команды с аргументами
@Getter @Setter public class Command2 extends AppCommand { private String someParams; public Command2() { super(); } public Command2(String someParams) { this.someParams = someParams; } public Command2(Map<String, String> arguments, String someParams) { super(arguments); this.someParams = someParams; } }
Enum для определения принадлежности команды к классу, посредством метода getByClass()
public enum AppCommandsEnum { COMMAND1(Command1.class), COMMAND2(Command2.class); private Class<? extends AppCommand> appCommandClass; AppCommandsEnum(Class<? extends AppCommand> appCommandClass) { this.appCommandClass = appCommandClass; } public static AppCommandsEnum getByClass(Class<? extends AppCommand> appCommandClass) { for (AppCommandsEnum commandsEnum : AppCommandsEnum.values()) { if (commandsEnum.appCommandClass.isAssignableFrom(appCommandClass)) return commandsEnum; } return null; } public Class getAppCommandClass() { return appCommandClass; } }
Диспетчеру нужен инвокер (класс, вызывающий методы).
class CommandInvoker { private final Method method; private final Object instance; CommandInvoker(Method method, Object instance) { this.method = method; this.instance = instance; } void invoke(AppCommand command) { try { method.invoke(instance, command); } catch (IllegalAccessException | InvocationTargetException e) { throw new DispatchException(e); } catch (IllegalArgumentException e) { throw new MethodArgumentMismatchException( "Невалидные аргументы для метода: " + this.method.getName() + " :: " + this.instance.getClass().getName(), e); } } }
Диспетчер
@Component public class CommandDispatcher { private static Map<AppCommandsEnum, CommandInvoker> warehouse = new ConcurrentHashMap<>(); private final ApplicationContext context; @Autowired public CommandDispatcher(ApplicationContext context) { this.context = context; loadDispatchers(); } private void loadDispatchers() { Reflections reflections = new Reflections("com.xeofus.reflectiveapp.handler"); Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(CommandHandler.class); for (Class clazz : annotated) { for (Method method : clazz.getDeclaredMethods()) { HandlerMethod handlerMethod; if ((handlerMethod = method.getAnnotation(HandlerMethod.class)) != null) { Object beanObject = null; try { beanObject = context.getBean(clazz); } catch (Exception e) { System.out.println("Bean not found " + clazz); } if (beanObject != null) warehouse.put( handlerMethod.value(), new CommandInvoker(method, beanObject) ); } } } } public void dispatch(AppCommand command) { try { warehouse.get(AppCommandsEnum.getByClass(command.getClass())).invoke(command); } catch (NullPointerException e) { throw new MethodNotImplementedException("Для команды " + AppCommandsEnum.getByClass(command.getClass()) + " не нашлось аннотированного метода", e ); } } }
Немного заострю внимание, разберем чем занят диспетчер.
private static Map<AppCommandsEnum, CommandInvoker> warehouse = new ConcurrentHashMap<>();
Здесь используется статичный ConcurrentHashMap, о котором можно прочесть тут. Если коротко то, у HashMap synсhronized блоки, а у ConcurrentHashMap данные сегментированы и разбиты по hash’у ключа. Это дает доступ к данным с локом по сегментам, а не по объекту. В итоге есть мапа, который будем использовать как склад с бинами после скана CLASSPATH.
Далее идет DI и вызов из конструктора метода loadDispatchers()
@Autowired public CommandDispatcher(ApplicationContext context) { this.context = context; loadDispatchers(); }
Инициализируем диспетчера.
Определяем пакет для скана
Reflections reflections = new Reflections("com.xeofus.reflectiveapp.handler");
Получаем все классы аннотированные кастомной аннотацией @CommandHandler
Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(CommandHandler.class);
Дракарис
//Проходимся по каждому классу for (Class clazz : annotated) { //ищем задекларированные методы for (Method method : clazz.getDeclaredMethods()) { HandlerMethod handlerMethod; //если метод аннотирован нашей @HandlerMethod if ((handlerMethod = method.getAnnotation(HandlerMethod.class)) != null) { Object beanObject = null; // создаем бин вытащив из ApplicationContext // аннотированный класс try { //так делать не самая лучшая идея //нарушает философию IoC beanObject = context.getBean(clazz); } catch (Exception e) { System.out.println("Bean not found " + clazz); } //Далее, добавляем в ConcurrentHashMap if (beanObject != null) warehouse.put( handlerMethod.value(), new CommandInvoker(method, beanObject) ); } } }
Обработчик
public void dispatch(AppCommand command) { try { warehouse.get(AppCommandsEnum.getByClass(command.getClass())).invoke(command); } catch (NullPointerException e) { throw new MethodNotImplementedException("Для команды " + AppCommandsEnum.getByClass(command.getClass()) + " не нашлось аннотированного метода", e ); } }
Контроллер для WebSocket сообщений
@Controller public class WsController { private final CommandDispatcher dispatcher; private final ObjectMapper mapper; @Autowired public WsController(CommandDispatcher dispatcher, ObjectMapper mapper) { this.dispatcher = dispatcher; this.mapper = mapper; } @MessageMapping("/command/{commandName}") public void commandHandler( @DestinationVariable String commandName, Message message ) { try { @SuppressWarnings("unchecked") AppCommand appCommand = (AppCommand) mapper.readValue( new String((byte[]) message.getPayload()), AppCommandsEnum.valueOf(commandName.toUpperCase()).getAppCommandClass() ); dispatcher.dispatch(appCommand); } catch (IOException e) { e.printStackTrace(); } } }
Получаем команду вида /command/command1, переводим command1 в верхний регистр, вытаскиваем из AppCommandsEnum значение класса
AppCommandsEnum.valueOf(commandName.toUpperCase())
Код выше вернет enum COMMAND1, геттером getAppCommandClass() получаем класс handler Command1.class.
Плюс аргументы из message.getPayload().
new String((byte[]) message.getPayload())
Кастим все с это с помощью ObjectMapper.readValue() и отправляем обработчику диспетчера.
Итог: можем создавать бесконечное количество handler классов, именуя их как хотим, тем самым обеспечив контролируемое масштабирование. После каждого добавления handler класса, нужно объявить его в AppCommandsEnum
MYNEWCOMMAND(MyNewCommand.class),
теперь у нас готов новый endpoint /command/mynewcommand
Как вы уже поняли, endpoint должен совпадать с enum, это единственное ограничение.
Далее, нужны хэндлер классы с минимум одним методом, для выполнения той самой логики, ради которой пишется app. Классы аннотируем нашей аннотацией CommandHandler, методы аннотацией HandlerMethod
@CommandHandler public class MyCommand1 { private final WsCallbackDispatcher dispatcher; private final ObjectMapper mapper; @Autowired private MyCommand1(WsCallbackDispatcher dispatcher, ObjectMapper mapper) { this.dispatcher = dispatcher; this.mapper = mapper; } @HandlerMethod(AppCommandsEnum.COMMAND1) public void runExecution(Command1 command1) { //здесь блок выполнения какой-то бизнес логики Runnable run = () -> { try { dispatcher.dispatch(mapper.writeValueAsString(command1)); } catch (JsonProcessingException e) { e.printStackTrace(); } }; new Thread(run).start(); } }
Здесь облегченный вариант с интерфейсом Runnable, с минимумом трудозатрат можем использовать ExecutorService.
Мне кажется, труд выше получился немного громоздким и не самым простым для понимания. Если у вас есть альтернативное решение подобных задач, ну или просто критика, добро пожаловать в комментарии. В комментах рождается истина 🙂
Спасибо за внимание!
ссылка на оригинал статьи https://habr.com/ru/post/460871/
Добавить комментарий