Search code examples
javabyte-buddyorika

Byte Buddy Generated class not visible to Orika (Javaassist)


I use 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.

My problem is that the Orika mapper that is supposed to map to/from classes generated by Byte Buddy is not able to find them. The exception is thrown at this line in the Orika JavasssistCompilerStrategy class.

I'm not sure what happens here since the Class<?> type parameter is not null. Here's how my DTO class are generated:

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

I tried (but not insensively) to create a ByteBuddyCompilerStrategy for Orika, which would be a nice and clean solution, but I was unable to create fields and methods from string representation of them, like it is done in Javaassist. Orika holds a generated class definition in the SourceCodeContext class, which contains only source code (string representation) for fields and methods.

EDIT

Here's a quick representation of the (non-dynamic) base classes and interfaces. It will be a long post, but maybe it could help someone else in the future:

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

Now, I would like to be able to generated the DTO, Repository, Mapper, Service declaration, Service implementation and RestController from an entity class. For example, from this:

  @Entity
  public class FooEntityA extends BaseEntity {

    @Column private String columnA;

    // Getters and setters omitted for brevity
  }

I want to generate this:

  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
  }

And here's the method trying to do that (feel free to point out parts that could be better):

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

Now, when I use load(classLoader), I run into the Orika/Javassist problem. And when I use the load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT) way, and exception is thrown by the make() call when creating the service implementation class:

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

Maybe it is related to this ? For now my solution is to create the DTO class the same way the Entity class is, and use the load(classLoader). But I would like to generate the DTO class the same way all the other classes are.


Solution

  • Javassist probably relies on locating class files to do its work. With Byte Buddy, this is not necessarily possible as class files are injected such that a class loader cannot locate a class file from a jar file using the .getResource API.

    Have you tried .load(classLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT)? This strategy retains the class file such that Javassist could locate it.