Я использую 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 так же, как и все остальные классы.