Byte Buddy Generated class 对 Orika (Javaassist) 不可见

Byte Buddy Generated class not visible to Orika (Javaassist)

我用Byte Buddy to generate some DTO classes in a Spring Boot application. I also use the Orika mapper librarie to map Entity to/from DTO classes. This library uses another runtime code generation tool to generate mapper classes, which is Javassist.

我的问题是应该映射由 Byte Buddy 生成的 to/from classes 的 Orika 映射器找不到它们。 line in the Orika JavasssistCompilerStrategy class.

抛出异常

我不确定这里发生了什么,因为 Class<?> type 参数不为空。以下是我的 DTO class 的生成方式:

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();

我尝试(但不是不认真地)为 Orika 创建一个 ByteBuddyCompilerStrategy,这将是一个不错且干净的解决方案,但我无法从它们的字符串表示形式创建字段和方法,就像在 Javaassist. Orika holds a generated class definition in the SourceCodeContext class,其中仅包含字段和方法的源代码(字符串表示形式)。

编辑

这是(非动态)基础 classes 和接口的快速表示。这将是一个很长的 post,但也许它可以帮助将来的其他人:

  @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
  }

现在,我希望能够从实体 class 生成 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:
      // 
      // 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) 方式时,在创建服务实现 class:

make() 调用抛出异常
java.lang.TypeNotPresentException: Type com.example.FooDtoA not present

可能与有关?现在我的解决方案是像创建实体 class 一样创建 DTO class,并使用 load(classLoader)。但我想以与所有其他 class 相同的方式生成 DTO class。

Javassist 可能依赖于定位 class 文件来完成它的工作。对于 Byte Buddy,这不一定是可能的,因为 class 文件被注入,因此 class 加载程序无法使用 .getResource [=16] 从 jar 文件中找到 class 文件=].

你试过.load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT)了吗?此策略保留 class 文件,以便 Javassist 可以找到它。