Сгенерированный класс Byte Buddy не виден Орике (Javaassist)

Я использую Byte Buddy для создания некоторых классов DTO в приложении Spring Boot. Я также использую библиотеку сопоставления Orika для сопоставления Entity с классами DTO и из них. В этой библиотеке используется другой инструмент генерации кода во время выполнения для создания классов сопоставления, которым является Javassist.

Моя проблема в том, что картограф Orika, который должен сопоставлять классы, созданные Byte Buddy, не может их найти. Исключение выдается на этом в классе Orika JavasssistCompilerStrategy.

Я не уверен, что здесь происходит, поскольку параметр Class<?> type не равен нулю. Вот как генерируется мой класс DTO:

final ClassLoader classLoader = getClass().getClassLoader();

// This object holds the information required to generate classes
// for a use case (it also contains information about a generated
// @Repository, @Service and @RestController)
final DynamicBeansDefinition def = ...
// Create the DTO class from the entity class
final Class<?> entityClass = ClassUtils.forName(def.getEntityClassName(), classLoader);

final Builder<BaseDto> dtoBuilder =
      new ByteBuddy().subclass(BaseDto.class).name(def.getDtoClassName());

// Copy all the entity class properties (excluding inherited fields),
// adding @JsonView(Views.Public.class) on each field.
final Field[] fields = entityClass.getDeclaredFields();
for (final Field field : fields) {
  dtoBuilder
      .defineProperty(field.getName(), field.getType())
      .annotateField(
          AnnotationDescription.Builder.ofType(JsonView.class)
              .defineTypeArray("value", Views.Public.class)
              .build());
}
final Class<?> dtoClass = dtoBuilder.make().load(classLoader).getLoaded();

Я попытался (но не бездумно) создать ByteBuddyCompilerStrategy для Orika, что было бы хорошим и чистым решением, но мне не удалось создать поля и методы из их строкового представления, как это делается в Javaassist. Orika содержит сгенерированное определение класса в SourceCodeContext, который содержит только исходный код (строковое представление) для полей и методов.

ИЗМЕНИТЬ

Вот краткое представление (нединамических) базовых классов и интерфейсов. Это будет длинный пост, но, возможно, он может помочь кому-то еще в будущем:

  @MappedSuperclass
  public class BaseEntity {

    @Id private Long id;
    @Version private Long version;

    // Getters and setters omitted for brevity
  }

  @Entity
  public class SomeEntityA extends BaseEntity {
    // Fields, getters and setters omitted for brevity
  }

  @Entity
  public class SomeEntityB extends BaseEntity {
    // Fields, getters and setters omitted for brevity
  }

  public class BaseDto {

    @JsonView(Views.Summary.class)
    private Long id;

    @JsonView(Views.Summary.class)
    private Long version;

    // Getters and setters omitted for brevity
  }

  @NoRepositoryBean
  public interface BaseRepository<E extends BaseEntity> extends JpaRepository<E, Long> {
    // Methods omitted for brevity
  }

  @Repository
  public interface SomeRepositoryA extends BaseRepository<SomeEntityA> {}

  @Repository
  public interface SomeRepositoryB extends BaseRepository<SomeEntityB> {}

  public interface BaseService<E extends BaseEntity, D extends BaseDto> {
    // Methods omitted for brevity
  }

  public class BaseMapper<E extends BaseEntity, D extends BaseDto> {

    private SomeRepositoryA someRepositoryA;
    private SomeRepositoryB someRepositoryB;

    protected BaseMapper(SomeRepositoryA someRepositoryA, SomeRepositoryB someRepositoryB) {
      super();

      this.someRepositoryA = someRepositoryA;
      this.someRepositoryB = someRepositoryB;
    }

    // Implementation omitted for brevity
  }

  public class BaseServiceImpl<E extends BaseEntity, D extends BaseDto>
      implements BaseService<E, D> {

    private BaseRepository<E> repository;
    private SomeRepositoryA someRepositoryA;

    protected BaseServiceImpl(BaseRepository<E> repository, SomeRepositoryA someRepositoryA) {
      super();

      this.repository = repository;
      this.someRepositoryA = someRepositoryA;
    }

    // Implementation omitted for brevity
  }

  public class BaseController<E extends BaseEntity, D extends BaseDto> {

    private BaseService<E, D> service;

    protected BaseController(final BaseService<E, D> service) {
      super();

      this.service = service;
    }

    // Implementation omitted for brevity
  }

Теперь я хотел бы иметь возможность сгенерировать DTO, репозиторий, картограф, объявление службы, реализацию службы и RestController из класса сущностей. Например, отсюда:

  @Entity
  public class FooEntityA extends BaseEntity {

    @Column private String columnA;

    // Getters and setters omitted for brevity
  }

Я хочу сгенерировать это:

  public class FooDtoA extends BaseDto {

    @JsonView(Views.Public.class)
    private String columnA;

    // Getters and setters omitted for brevity
  }

  @Repository(value = "fooRepositoryA")
  public interface FooRepositoryA extends BaseRepository<FooEntityA> {
    // WILL ALWAYS BE EMPTY, everything is defined in the base interface
  }

  @Component(value = "fooMapperA")
  public class FooMapperA extends BaseMapper<FooEntityA, FooDtoA> {

    public FooMapperA(SomeRepositoryA someRepositoryA, SomeRepositoryB someRepositoryB) {
      super(someRepositoryA, someRepositoryB);
    }

    // WILL ALWAYS BE EMPTY, everything is defined in the base class
  }

  public interface FooServiceA extends BaseService<FooEntityA, FooDtoA> {
    // WILL ALWAYS BE EMPTY, everything is defined in the base interface
  }

  @Service(value = "fooServiceAImpl")
  public class ServiceAImpl extends BaseServiceImpl<FooEntityA, FooDtoA> implements FooServiceA {

    public ServiceAImpl(
        @Autowired @Qualifier(value = "fooRepositoryA") BaseRepository<FooEntityA> repository,
        SomeRepositoryA someRepositoryA) {
      super(repository, someRepositoryA);
    }

    // WILL ALWAYS BE EMPTY, everything is defined in the base class
  }

  @RestController(value = "fooControllerA")
  @RequestMapping(path = "fooPathA")
  public class FooControllerA extends BaseController<FooEntityA, FooDtoA> {

    public FooControllerA(
        @Autowired @Qualifier(value = "fooServiceAImpl") BaseService<FooEntityA, FooDtoA> service) {
      super(service);
    }

    // WILL ALWAYS BE EMPTY, everything is defined in the base class
  }

И вот метод, который пытается это сделать (не стесняйтесь указывать части, которые могли бы быть лучше):

  public void createBeans(
      final ConfigurableListableBeanFactory beanFactory, final BeansDefinition def) {

    final ClassLoader classLoader = getClass().getClassLoader();

    try {
      // Create the DTO class from the entity class
      final Class<?> entityClass = ClassUtils.forName(def.getEntityClassName(), classLoader);
      // final Class<?> dtoClass = ClassUtils.forName(def.getDtoClassName(), classLoader);

      final Builder<BaseDto> dtoBuilder =
          new ByteBuddy().subclass(BaseDto.class).name(def.getDtoClassName());

      // Copy all the entity class properties, adding
      // @JsonView(Views.Public.class) on each field.
      final Field[] fields = entityClass.getDeclaredFields();
      for (final Field field : fields) {
        dtoBuilder
            .defineProperty(field.getName(), field.getType())
            .annotateField(
                AnnotationDescription.Builder.ofType(JsonView.class)
                    .defineTypeArray("value", Views.Public.class)
                    .build());
      }
      final Class<?> dtoClass =
          dtoBuilder
              .make()
              // .load(classLoader)
              .load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT)
              .getLoaded();

      // Create the repository
      new ByteBuddy()
          .makeInterface(
              TypeDescription.Generic.Builder.parameterizedType(BaseRepository.class, entityClass)
                  .build())
          .name(def.getRepositoryClassName())
          .annotateType(
              AnnotationDescription.Builder.ofType(Repository.class)
                  .define("value", def.getRepositoryBeanName())
                  .build())
          .make()
          // .load(classLoader)
          .load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT)
          .getLoaded();

      // This is an ugly hack in order to create the same BeanDefinition for
      // our created Repository as if it was auto configured by spring
      // boot. There is no other way (AFAIK) to do this, since Spring
      // won't scan the dynamically created classes. See:
      // https://stackoverflow.com/questions/37402782
      // So the hack is to create a RootBeanDefinition from a known
      // existing repository RootBeanDefinition, and then to change
      // the argument that will be used to create the
      // JpaRepositoryFactoryBean.
      final RootBeanDefinition repositoryABeanDefinition =
          (RootBeanDefinition) beanFactory.getBeanDefinition("someRepositoryA");
      final RootBeanDefinition repositoryBeanDefinition =
          new RootBeanDefinition(repositoryABeanDefinition);
      repositoryBeanDefinition.getConstructorArgumentValues().clear();
      repositoryBeanDefinition
          .getConstructorArgumentValues()
          .addIndexedArgumentValue(0, def.getRepositoryClassName());
      ((DefaultListableBeanFactory) beanFactory)
          .registerBeanDefinition(def.getRepositoryBeanName(), repositoryBeanDefinition);

      // Create the service mapper
      final Class<?> mapperClass =
          new ByteBuddy()
              .subclass(
                  TypeDescription.Generic.Builder.parameterizedType(
                          BaseMapper.class, entityClass, dtoClass)
                      .build(),
                  ConstructorStrategy.Default.NO_CONSTRUCTORS)
              .name(def.getMapperClassName())
              .annotateType(
                  AnnotationDescription.Builder.ofType(Component.class)
                      .define("value", def.getMapperBeanName())
                      .build())
              .defineConstructor(Modifier.PUBLIC)
              .withParameters(SomeRepositoryA.class, SomeRepositoryB.class)
              .intercept(
                  MethodCall.invoke(
                          BaseMapper.class.getDeclaredConstructor(
                                  SomeRepositoryA.class, SomeRepositoryB.class))
                      .withArgument(0, 1))
              .make()
              // .load(classLoader)
              .load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT)
              .getLoaded();

      final BeanDefinition mapperBeanDefinition = new RootBeanDefinition(mapperClass);
      ((DefaultListableBeanFactory) beanFactory)
          .registerBeanDefinition(def.getMapperBeanName(), mapperBeanDefinition);

      // Create the service interface
      final Class<?> serviceInterfaceClass =
          new ByteBuddy()
              .makeInterface(
                  TypeDescription.Generic.Builder.parameterizedType(
                          BaseService.class, entityClass, dtoClass)
                      .build())
              .name(def.getServiceInterfaceClassName())
              .make()
              //.load(classLoader)
               .load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT)
              .getLoaded();

      // Create the service implementation
      final Class<?> serviceImplClass =
          new ByteBuddy()
              .subclass(
                  TypeDescription.Generic.Builder.parameterizedType(
                          BaseServiceImpl.class, entityClass, dtoClass)
                      .build(),
                  ConstructorStrategy.Default.NO_CONSTRUCTORS)
              .name(def.getServiceImplementationClassName())
              .implement(serviceInterfaceClass)
              .annotateType(
                  AnnotationDescription.Builder.ofType(Service.class)
                      .define("value", def.getServiceBeanName())
                      .build())
              .defineConstructor(Modifier.PUBLIC)
              .withParameter(BaseRepository.class)
              .annotateParameter(
                  AnnotationDescription.Builder.ofType(Autowired.class).build(),
                  AnnotationDescription.Builder.ofType(Qualifier.class)
                      .define("value", def.getRepositoryBeanName())
                      .build())
              .withParameter(SomeRepositoryA.class)
              .intercept(
                  MethodCall.invoke(
                          BaseServiceImpl.class.getDeclaredConstructor(
                                  BaseRepository.class, SomeRepositoryA.class))
                      .withArgument(0, 1))
              .make()
              //.load(classLoader)
              .load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT)
              .getLoaded();

      final BeanDefinition serviceBeanDefinition = new RootBeanDefinition(serviceImplClass);
      ((DefaultListableBeanFactory) beanFactory)
          .registerBeanDefinition(def.getServiceBeanName(), serviceBeanDefinition);

      // Create the rest controller
      final Class<?> controllerClass =
          new ByteBuddy()
              .subclass(
                  TypeDescription.Generic.Builder.parameterizedType(
                          BaseController.class, entityClass, dtoClass)
                      .build(),
                  ConstructorStrategy.Default.NO_CONSTRUCTORS)
              .name(def.getControllerClassName())
              .annotateType(
                  AnnotationDescription.Builder.ofType(RestController.class)
                      .define("value", def.getControllerBeanName())
                      .build(),
                  AnnotationDescription.Builder.ofType(RequestMapping.class)
                      .defineArray("value", def.getControllerPath())
                      .build())
              .defineConstructor(Modifier.PUBLIC)
              .withParameter(BaseService.class)
              .annotateParameter(
                  AnnotationDescription.Builder.ofType(Autowired.class).build(),
                  AnnotationDescription.Builder.ofType(Qualifier.class)
                      .define("value", def.getServiceBeanName())
                      .build())
              .intercept(
                  MethodCall.invoke(
                          BaseController.class.getDeclaredConstructor(BaseService.class))
                      .withArgument(0))
              .make()
              //.load(classLoader)
              .load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT)
              .getLoaded();

      final BeanDefinition controllerBeanDefinition = new RootBeanDefinition(controllerClass);
      ((DefaultListableBeanFactory) beanFactory)
          .registerBeanDefinition(def.getControllerBeanName(), controllerBeanDefinition);

    } catch (Exception ex) {
      throw new FatalBeanException("Unable to create beans for entity " + def.getEntityName(), ex);
    }
  }

Теперь, когда я использую load(classLoader), я сталкиваюсь с проблемой Orika/Javassist. И когда я использую способ load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT), и вызов make() вызывает исключение при создании класса реализации службы:

java.lang.TypeNotPresentException: Type com.example.FooDtoA not present

Может быть, это связано с этим? На данный момент мое решение состоит в том, чтобы создать класс DTO так же, как класс Entity, и использовать класс load(classLoader). Но я хотел бы создать класс DTO так же, как и все остальные классы.


person sbraconnier    schedule 10.04.2018    source источник


Ответы (1)


Javassist, вероятно, полагается на поиск файлов классов для выполнения своей работы. С Byte Buddy это не обязательно возможно, так как файлы классов внедряются таким образом, что загрузчик классов не может найти файл класса из jar-файла с помощью .getResource API.

Вы пробовали .load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT)? Эта стратегия сохраняет файл класса таким образом, чтобы Javassist мог его найти.

person Rafael Winterhalter    schedule 11.04.2018
comment
Спасибо! К сожалению, это не сработало. Я столкнулся с другой проблемой. Я только что отредактировал свой исходный вопрос, чтобы подробно объяснить, что это за новая проблема и что именно я пытаюсь сделать. - person sbraconnier; 12.04.2018