Ленивые строители объектов с бобом Spring - PullRequest
1 голос
/ 05 июня 2019

Я играю с идеей использовать подобный подход, который могут сделать классы @Configuration, который может лениво создавать bean-компоненты с вызовами методов @Bean и возвращать существующие объекты, если они уже были вызваны. Это делается с помощью прокси CGLib.

Одна интересная особенность заключается в том, что он работает даже при вызове метода сам по себе:

@Configuration
class Config {
    @Bean ClassA beanA() {
        return new ClassA(beanB());
    }

    @Bean ClassB beanB() {
        return new ClassB();
    }
}

Теперь, в моем случае использования, не касающемся конфигурации Spring, я хочу использовать эту возможность для ленивого создания произвольных графов объектов (которые не должны быть Spring Beans), вызывая метод Bean-компонента Builder , который создаст объекты, если они еще не вызваны, и вернет существующие объекты, если они уже были вызваны. А также я хочу использовать возможность самостоятельного вызова методов в одном экземпляре. До сих пор я не мог этого сделать.

Как я могу создавать и улучшать Spring Beans (как прокси-серверы CGLib) , чтобы они могли самостоятельно вызывать методы , аналогично классам @Configuration, но с моими собственными советами по обработке лени и кеширование?


РЕДАКТИРОВАТЬ: более подробно

Результат, в конце концов, должен выглядеть аналогично приведенному выше примеру конфигурации, но это будет обычный синглтон-компонент Spring:

@Component
@MyBuilder // or some other custom annotation
class MyObjectGraphBuilder {
    @Builder ClassA objectA() {
        return new ClassA(objectB());
    }

    @Builder ClassB objectB() {
        return new ClassB();
    }
}

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

Кэширование результатов вызова метода является простым (может быть сделано AOP), но мне нужна возможность самовывоза, которая обычно не поддерживается Spring, если это не класс @Configuration.

Я подумал, что Spring делает это, улучшая классы @Configuration bean-компонентами с помощью собственных прокси-серверов CGlib. Тем не менее, это включает в себя много копирования и настройки (например, ConfigurationClassEnhancer, ConfigurationClassPostProcessor и т. Д.), И до сих пор мне не повезло на самом деле заставить его работать с моим пользовательским Post Processor и Enhancer (код слишком длинный, но это в основном копия из упомянутых классов и написание моих пользовательских методов перехватчиков). Поэтому я пытаюсь выяснить, существует ли другой способ.

1 Ответ

2 голосов
/ 13 июня 2019

Простой ответ, касающийся AOP и самостоятельного вызова: вы не можете использовать Spring AOP, вы должны использовать полный AspectJ. Хорошей новостью является то, что вам не нужны прокси для этого решения. В руководстве Spring описано, как использовать AspectJ из Spring через LTW (ткачество во время загрузки) . Не беспокойтесь, если настроено правильно, вы можете использовать AspectJ наряду с другими аспектами, реализованными через Spring AOP. Кроме того, если вам не нравится LTW, вы также можете использовать ткачество во время компиляции через плагин AspectJ Maven.

Теперь приведем небольшой пример кэширования на чистом Java + AspectJ (без участия Spring) для демонстрации:

Примечание строителя:

package de.scrum_master.app;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(METHOD)
public @interface Builder {}

Образцы классов:

package de.scrum_master.app;

public class ClassB {
  @Override
  public String toString() {
    return "ClassB@" + hashCode();
  }
}
package de.scrum_master.app;

public class ClassA {
  private ClassB objectB;

  public ClassA(ClassB objectB) {
    this.objectB = objectB;
  }

  @Override
  public String toString() {
    return "ClassA@" +hashCode() + "(" + objectB + ")";
  }
}

Приложение драйвера с аннотированными заводскими методами:

package de.scrum_master.app;

public class MyObjectGraphBuilder {
  @Builder
  ClassA objectA() {
    return new ClassA(objectB());
  }

  @Builder
  ClassB objectB() {
    return new ClassB();
  }

  public static void main(String[] args) {
    MyObjectGraphBuilder builder = new MyObjectGraphBuilder();
    System.out.println(builder.objectB());
    System.out.println(builder.objectA());
    System.out.println(builder.objectB());
    System.out.println(builder.objectA());
    System.out.println(builder.objectB());
    System.out.println(builder.objectA());
  }
}

Журнал консоли без аспекта кэширования:

ClassB@1829164700
ClassA@2018699554(ClassB@1311053135)
ClassB@118352462
ClassA@1550089733(ClassB@865113938)
ClassB@1442407170
ClassA@1028566121(ClassB@1118140819)

Пока все так предсказуемо. Это нормальное поведение, никакого кеширования нет.

Кэширование:

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

package de.scrum_master.app;

import java.util.HashMap;
import java.util.Map;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

@Aspect
public class BuilderCacheAspect {
  private Map<Class<?>, Object> cachedObjects = new HashMap<>();

  @Around("@annotation(de.scrum_master.app.Builder) && execution(* *(..))")
  public Object findOrCreateObject(ProceedingJoinPoint thisJoinPoint) throws Throwable {
    //System.out.println(thisJoinPoint);
    Class<?> returnType = ((MethodSignature) thisJoinPoint.getSignature()).getReturnType();
    Object cachedObject = cachedObjects.get(returnType);
    if (cachedObject == null) {
      cachedObject = thisJoinPoint.proceed();
      cachedObjects.put(returnType, cachedObject);
    }
    return cachedObject;
  }
}

Журнал консоли с аспектом кэширования:

ClassB@1392838282
ClassA@664740647(ClassB@1392838282)
ClassB@1392838282
ClassA@664740647(ClassB@1392838282)
ClassB@1392838282
ClassA@664740647(ClassB@1392838282)

Tadaa! Есть наш простой объектный кеш. Наслаждайтесь.

...