Как сгенерировать реализацию службы из нескольких реализаций во время выполнения?

Допустим, у меня есть следующие интерфейсы:

public interface FindExtension {
    void findById(long id);
    void findAll();
}

public interface SaveExtension {
    void save();
}

public interface DeleteExtension {
    void deleteById(long id);
}

И у меня есть следующие реализации:

public class FindExtensionImpl implements FindExtension {

    @Override
    public void findById(long id) {
        System.out.println("FindExtensionImpl::findById(" + id + ")");
    }

    @Override
    public void findAll() {
        System.out.println("FindExtensionImpl::findAll()");
    }
}

public class SaveExtensionImpl implements SaveExtension {

    @Override
    public void save() {
        System.out.println("SaveExtensionImpl::save()");
    }
}

public class DeleteExtensionImpl implements DeleteExtension {
    @Override
    public void deleteById(long id) {
        System.out.println("DeleteExtensionImpl::deleteById(" + id + ")");
    }
}

И теперь я хочу смешать эти реализации и, допустим, мне нужен этот сервис:

public interface MyService extends FindExtension, SaveExtension, DeleteExtension {
}

Теперь в моем приложении Spring Boot я хотел бы внедрить реализацию MyService в один из моих контроллеров. Обычно я создавал класс MyServiceImpl, реализовывал методы интерфейса и аннотировал @Service. Таким образом, Spring Boot просканирует мой код, создаст экземпляр этой службы и предоставит его мне в любое время, когда мне это нужно.

Однако я бы хотел не создавать MyServiceImpl, а генерировать его фреймворком во время выполнения, другими словами, состоять из маленьких кусочков. Как мне сказать Spring Boot делать это автоматически? Или мне нужно создать свой собственный процессор аннотаций и аннотаций, который каким-то образом сгенерирует реализацию?

Это что-то похожее на репозитории Spring Boot, где у меня будет такой интерфейс:

@Repository
interface IPostRepository implements JpaRepository<Post,Long>, QuerydslPredicateExecutor<Post>, PostCustomRepositoryExtension { }

И Spring Boot "волшебным образом" создаст реализацию этого интерфейса и внедрит методы из JpaRepository, QuerydslPredicateExecutor и моего собственного PostCustomRepositoryExtension...

Поскольку Spring Boot уже выполняет логику, аналогичную моей, мне интересно, могу ли я повторно использовать это и как?

Забудьте о весне и подумайте о простой Java. Ни одна из трех показанных вами реализаций не реализует MyService, они просто реализуют одну его часть. Если вам нужна пригодная для использования реализация MyService, вам все равно придется инкапсулировать три небольшие реализации в одну большую, которая фактически реализует весь сервис. Так как вы не можете сделать это в простой Java, вы не сможете сделать это в Spring без обработки пользовательских аннотаций.

Matteo NNZ 05.10.2022 09:46

Я думаю, вам придется что-то реализовать самостоятельно — не обязательно процессор аннотаций, но я бы сказал, что это больше похоже на фабрику компонентов, которая может предоставлять экземпляры MyService. Из того, что я знаю, Spring делает это так, то есть создает прокси во время выполнения. Вы можете взглянуть на такие классы, как RepositoryFactorySupport и т. д. - Однако я бы посоветовал вам подумать, действительно ли это стоит усилий, поскольку могут быть более простые решения с несколько другим дизайном.

Thomas 05.10.2022 09:58
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
2
176
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Самый простой способ - поместить весь интерфейс в один интерфейс:

public interface CommonService<T, Z> {
    void findById(long id);
    void findAll();
    void save();
    void deleteById(long id);
}

затем сделайте свой сервис, который расширяет CommonService

public interface ObjectService extends CommonService<Object, long> {

}

и, наконец, вы можете сделать импл, который реализует ObjectService

public class ObjectImpl implements ObjectService {
@Override
    public void findById(long id) {
        System.out.println("ObjectImpl::findById(" + id + ")");
    }

    @Override
    public void findAll() {
        System.out.println("ObjectImpl::findAll()");
    }

    @Override
    public void save() {
        System.out.println("ObjectImpl::save()");
    }

    @Override
    public void deleteById(long id) {
        System.out.println("ObjectImpl::deleteById(" + id + ")");
    }

}

Вам не нужно объединять все эти методы в один интерфейс. MyService расширение нескольких интерфейсов — это нормально. Кроме того, ОП знает о возможности создания единой реализации общего интерфейса, которая в вашем примере будет ObjectImpl. Однако вопрос заключается в том, есть ли способ автоматически создать составную реализацию из фрагментов, каждый из которых реализует только один из меньших интерфейсов.

Thomas 05.10.2022 10:44

@Thomas, вот почему он называется CommonService, это просто обычный, самый простой способ.

LunaLissa 05.10.2022 10:54

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

Thomas 05.10.2022 11:18
Ответ принят как подходящий

Отказ от ответственности

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

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

Подход

Теперь вот подход:

  • Создайте пользовательскую аннотацию, чтобы отметить интересующие вас интерфейсы.
  • Зарегистрируйте фабричные методы, которые создают прокси для этих интерфейсов. Эти прокси будут делегировать вызовы соответствующим реализациям «фрагментов».
  • Включите все с аннотацией в вашем классе приложения.

Код (только дополнительные соответствующие части)

Аннотация интерфейса:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CompositeService {}

Аннотация включения:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({CompositeServiceFactoryRegistrar.class})
public @interface EnableCompositeServices {}

Главный заводской регистратор (со встроенными комментариями):

@Component
public class CompositeServiceFactoryRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    private Map<Class<?>, Supplier<Object>> fragmentFactories = new HashMap<>();

    private Environment environment;
   
    public CompositeServiceFactoryRegistrar() {
        //hard coded fragment registration, you'll probably want to do a lookup of all interfaces and their implementation on the classpath instead
        fragmentFactories.put(SaveExtension.class, () -> new SaveExtensionImpl());
        fragmentFactories.put(DeleteExtension.class, () -> new DeleteExtensionImpl());      
        fragmentFactories.put(FindExtension.class, () -> new FindExtensionImpl());
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        
        //get the enablement annotation and set up package scan
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(EnableCompositeServices.class.getCanonicalName());

        if (annotationAttributes != null) {
            String[] basePackages = (String[]) annotationAttributes.get("value");

            if (basePackages == null || basePackages.length == 0) {
                // If value attribute is not set, fallback to the package of the annotated class
                basePackages = new String[] {
                        ((StandardAnnotationMetadata) metadata).getIntrospectedClass().getPackage().getName() };
            }

            // using these packages, scan for interface annotated with MyCustomBean
            ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(
                    false, environment) {
                // Override isCandidateComponent to only scan for interface
                @Override
                protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                    AnnotationMetadata metadata = beanDefinition.getMetadata();
                    return metadata.isIndependent() && metadata.isInterface();
                }
            };
            provider.addIncludeFilter(new AnnotationTypeFilter(CompositeService.class));

            // Scan all packages
            for (String basePackage : basePackages) {
                for (BeanDefinition beanDefinition : provider.findCandidateComponents(basePackage)) {                   

                    GenericBeanDefinition genericDef = new GenericBeanDefinition(beanDefinition);
                    
                    //resolve the interface class if not done yet
                    if ( !genericDef.hasBeanClass()) {
                        try {
                            genericDef.resolveBeanClass(getClassLoader());
                        } catch(ClassNotFoundException e) {
                            //simple logging, replace that with something more appropriate
                            e.printStackTrace();
                        }
                    }
                    
                    Class<?> interfaceType = genericDef.getBeanClass();                                 

                    //add the factory to the bean definition and then register it
                    genericDef.setInstanceSupplier(() -> createProxy(interfaceType) );                  
                    registry.registerBeanDefinition(interfaceType.getSimpleName(), genericDef);
                }
            }
        }
    }

    /*
     * Main factory method
     */
    @SuppressWarnings("unchecked")
    private <T> T createProxy(Class<T> type) {
        //create the factory and set the interface type
        ProxyFactory factory = new ProxyFactory();
        factory.setInterfaces(type);

        //add the advice that actually delegates to the fragments
        factory.addAdvice(new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                Method invokedMethod = invocation.getMethod();

                Class<?> invokedClass = invokedMethod.getDeclaringClass();
                if (invokedClass.isInterface()) {

                    //create the fragment for this method, if not possible continue with the next interceptor
                    Supplier<Object> supplier = fragmentFactories.get(invokedClass);
                    if (supplier == null) {
                        return invocation.proceed();
                    }

                    Object fragment = supplier.get();

                    //get the fragment method and invoke it
                    Method targetMethod = fragment.getClass().getDeclaredMethod(invokedMethod.getName(),
                            invokedMethod.getParameterTypes());

                    return targetMethod.invoke(fragment, invocation.getArguments());
                } else {
                    return invocation.proceed();
                }
            }
        });

        return (T) factory.getProxy(getClassLoader());
    }
    
    private ClassLoader getClassLoader() {
        return getClass().getClassLoader();
    }
}

Обратите внимание, что это очень простая реализация и имеет несколько недостатков, например.

  • он не обрабатывает перегрузки и преобразование параметров
  • у него могут быть трудности с переопределением -> нужно больше магии отражения
  • фрагменты жестко закодированы
  • (подозреваю много других)

Спасибо за очень хорошее объяснение, я еще не проверял его, но спасибо за указание в правильном направлении.

clzola 05.10.2022 19:54

Другие вопросы по теме