Бои SpringBoot: динамическая инъекция генератора идентификаторов через BeanPostProcessor

Spring Boot задняя часть
Бои SpringBoot: динамическая инъекция генератора идентификаторов через BeanPostProcessor

«Это 23-й день моего участия в ноябрьском испытании обновлений. Подробную информацию об этом событии см.:Вызов последнего обновления 2021 г."

Эта статья была«Весенняя загрузка в действии»Коллекция столбцов.

Привет, я смотрю на гору.

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

В настоящее время существует множество классических методов генерации идентификаторов, таких как столбец автоинкремента базы данных (первичный ключ или последовательность автоинкремента), алгоритм Snowflake, алгоритм Meituan Leaf и т. д. Следовательно, будет какой-то уровень компании или бизнес-уровня. Компоненты генератора ID. Эта статья представляет собой настоящую борьбу с динамическим внедрением генератора идентификаторов через BeanPostProcessor.

В Spring существует множество способов реализации инъекции, например, запуск Springboot, bean-компонент генератора идентификаторов инициализируется в пользовательской конфигурации, а бизнес-код передается через@AutoWiredили@ResourceПросто введите его прямо из коробки. Этот метод прост и прямолинеен, но недостатком является то, что он слишком прост и не имеет пользовательского ввода.

Рассмотрим реальный сценарий: в одном и том же бизнес-документе необходимо сохранить уникальный идентификатор, но в разных документах он может повторяться. Более того, когда эти алгоритмы генерируют идентификаторы, чтобы сохранить уникальные результаты, возвращаемые несколькими потоками, они блокируют общие ресурсы. Если разные службы имеют разные сценарии параллелизма, службы с низким уровнем параллелизма могут быть заблокированы службами с высоким уровнем параллелизма для получения идентификаторов, что приводит к некоторым потерям производительности. Поэтому мы должны рассмотреть возможность изоляции генератора идентификаторов в соответствии с бизнесом, чтобы стартер Springboot не был достаточно гибким.

выполнить

Согласно вышеуказанным требованиям, мы можем реализовать нашу логику в несколько шагов:

  1. Аннотация пользовательского свойства, используемая для определения необходимости внедрения объекта свойства.
  2. Определите интерфейс генератора идентификаторов, класс реализации и класс фабрики, класс фабрики предназначен для создания различных объектов реализации генератора идентификаторов в соответствии с определением.
  3. Определите BeanPostProcessor, найдите свойства, определенные с помощью пользовательских аннотаций, и внедрите внедрение

пользовательская аннотация

Сначала настройте аннотацию, вы можете определитьvalueАтрибут, как идентификатор изолированного бизнеса:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface IdGeneratorClient {
    /**
     * ID 生成器名称
     *
     * @return
     */
    String value() default "DEFAULT";
}

Определить генератор идентификаторов

Определите интерфейс для генератора идентификаторов:

public interface IdGenerator {
    String groupName();

    long nextId();
}

Реализуйте интерфейс генератора идентификаторов и используйте его ленивоAtomicLongРеализовать автоинкремент с учетом того, что генератор ID сгруппирован поConcurrentHashMapРеализуйте генератор идентификаторов, содержащий:

class DefaultIdGenerator implements IdGenerator {
    private static final Map<String, AtomicLong> ID_CACHE = new ConcurrentHashMap<>(new HashMap<>());
    private final String groupName;

    DefaultIdGenerator(final String groupName) {
        this.groupName = groupName;
        synchronized (ID_CACHE) {
            ID_CACHE.computeIfAbsent(groupName, key -> new AtomicLong(1));
        }
    }

    @Override
    public String groupName() {
        return this.groupName;
    }

    @Override
    public long nextId() {
        return ID_CACHE.get(this.groupName).getAndIncrement();
    }
}

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

public enum IdGeneratorFactory {
    INSTANCE;

    private static final Map<String, IdGenerator> ID_GENERATOR_MAP = new ConcurrentHashMap<>(new HashMap<>());

    public synchronized IdGenerator create(final String groupName) {
        return ID_GENERATOR_MAP.computeIfAbsent(groupName, key -> new DefaultIdGenerator(groupName));
    }
}

Определить BeanPostProcessor

Все первые являются базовыми операциями, а вот ядро ​​расширения. Наша логика реализации такова:

  1. Сканирует все свойства бина и находитIdGeneratorClientАннотированные свойства
  2. получать аннотацииvalueзначение, как идентификатор группы для генератора идентификаторов
  3. использоватьIdGeneratorFactoryЭтот фабричный класс генерирует экземпляры генератора идентификаторов, которые будут возвращать новые или уже определенные экземпляры.
  4. Записать экземпляр генератора идентификаторов в bean-компонент через отражение
public class IdGeneratorBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
        parseFields(bean);
        return bean;
    }

    private void parseFields(final Object bean) {
        if (bean == null) {
            return;
        }
        Class<?> clazz = bean.getClass();
        parseFields(bean, clazz);

        while (clazz.getSuperclass() != null && !clazz.getSuperclass().equals(Object.class)) {
            clazz = clazz.getSuperclass();
            parseFields(bean, clazz);
        }
    }

    private void parseFields(final Object bean, Class<?> clazz) {
        if (bean == null || clazz == null) {
            return;
        }

        for (final Field field : clazz.getDeclaredFields()) {
            try {
                final IdGeneratorClient annotation = AnnotationUtils.getAnnotation(field, IdGeneratorClient.class);
                if (annotation == null) {
                    continue;
                }

                final String groupName = annotation.value();

                final Class<?> fieldType = field.getType();
                if (fieldType.equals(IdGenerator.class)) {
                    final IdGenerator idGenerator = IdGeneratorFactory.INSTANCE.create(groupName);
                    invokeSetField(bean, field, idGenerator);
                    continue;
                }

                throw new RuntimeException("未知字段类型无法初始化,bean: " + bean + ",field: " + field);
            } catch (Throwable t) {
                throw new RuntimeException("初始化字段失败,bean=" + bean + ",field=" + field, t);
            }
        }
    }

    private void invokeSetField(final Object bean, final Field field, final Object param) {
        ReflectionUtils.makeAccessible(field);
        ReflectionUtils.setField(field, bean, param);
    }
}

выполнитьBeanPostProcessorИнтерфейс надо делатьpostProcessBeforeInitializationиpostProcessAfterInitializationОпределение двух методов. На следующем рисунке показан процесс создания экземпляров Beans в Spring:

图片

Схема процесса инстанцирования bean-компонентов в Spring

Как видно из рисунка, Spring вызываетBeanPostProcessorПри использовании этих двух методов создается экземпляр bean-компонента, внедряются все внедряемые свойства, и он является законченным bean-компонентом. И возвращаемое значение двух методов может быть исходным экземпляром компонента или обернутым экземпляром, что зависит от нашего определения.

проверить наш код

Напишите тестовый пример, чтобы убедиться, что наша реализация работает:

@SpringBootTest
class SpringBeanPostProcessorApplicationTests {
    @IdGeneratorClient
    private IdGenerator defaultIdGenerator;
    @IdGeneratorClient("group1")
    private IdGenerator group1IdGenerator;

    @Test
    void contextLoads() {
        Assert.notNull(defaultIdGenerator, "注入失败");
        System.out.println(defaultIdGenerator.groupName() + " => " + defaultIdGenerator.nextId());

        Assert.notNull(group1IdGenerator, "注入失败");
        for (int i = 0; i < 5; i++) {
            System.out.println(defaultIdGenerator.groupName() + " => " + defaultIdGenerator.nextId());
            System.out.println(group1IdGenerator.groupName() + " => " + group1IdGenerator.nextId());
        }
    }

}

Текущий результат:

DEFAULT => 1
DEFAULT => 2
group1 => 1
DEFAULT => 3
group1 => 2
DEFAULT => 4
group1 => 3
DEFAULT => 5
group1 => 4
DEFAULT => 6
group1 => 5

Как видите, генератор идентификаторов по умолчанию и генератор идентификаторов с именем определения group1 генерируются отдельно, как и ожидалось.

Мысли в конце статьи

Мы достигли черезBeanPostProcessorЧтобы добиться автоматического внедрения пользовательских бизнес-объектов, приведенная выше реализация относительно проста, и есть много мест, которые можно расширить, например, реализация фабричного метода, который может более гибко создавать объекты генератора идентификаторов с помощью SPI. В то же время, учитывая распределенный сценарий, мы также можем реализовать логику удаленной генерации идентификатора, внедрив экземпляры rpc в класс реализации генератора идентификаторов.

Геймплей ничем не ограничен, все зависит от нашего воображения. Вы можете обратить внимание на публичный аккаунт «Хижина Горы» и ответить «весне», чтобы получить исходный код.

Рекомендуемое чтение


Привет, я смотрю на гору. Плавайте в мире кода, играйте и наслаждайтесь жизнью. Если статья была вам полезна, ставьте лайк, добавляйте в закладки и подписывайтесь. Приглашаем обратить внимание на паблик-аккаунт «Глядя на горную хижину» и открыть для себя другой мир.