Привет всем! Любой программист, хоть немного знающий Java работал с такой штукой, как generic. Эта фича появилась аж в 5-ой версии Java и сегодня я хотел бы рассказать о некоторых нетривиальных проблемах, связанных с обобщенными типами, с которыми я сталкивался, а также о том почему они возникают и как их можно решить. В этой статье также будут затронуты всеми (не)любимые Hibernate и Spring.
Но начну я с объяснения некоторых тонкостей generic'ов, которые не всегда понимают новички в мире Java. Если вы опытный разработчик, то можете не читать первые два пункта.
1) Зачем нужен wildcard, extend, super
Wildcard (?) используется во время подстановки в обобщенный класс. Означает он, что нам не важно каким будет параметризованный тип или, если wildcard используется вкупе с ключевыми словами super и extend, то нам важно только, чтобы параметризованный тип был родительским или расширял определенный класс. Приведу понятный пример, как это используется на практике.
public void foobar(Map<String, Object> ms) {
...
}
Если мы захотим передать в метод переменную типа Map<String, Number>, то ничего не получиться (почему — расскажу в следующем пункте), но если метод будет объявлен так, то нам это удастся.
public void foobar(Map<String, ?> ms) {
...
}
То есть во втором случае мы говорим, о том что нам не важно какого типа будет лежать value в мапе. Но это накладывает и свои ограничения, теперь у значений мапы мы сможем вызывать только методы класса Object. Если мы знаем, что значениями в этой мапе могут быть только объекты класса Number, то мы можем переписать сигнатуру метода так.
public void foobar(Map<String, ? extend Number> ms) {
...
}
Теперь у значений мапы нам доступны методы класса Number. Возникает вопрос, зачем же нам тогда ключевое слово super? Оно говорит о том, что параметризованный тип будет родителем для определенного класса, но это не дает возможность полиморфизма — вызывать метод какого-либо класса, кроме базового для всех — Object. Опять же приведу пример.
List<? extends Number> list = new ArrayList<Number>();
List<? extends Number> list = new ArrayList<Integer>();
List<? extends Number> list = new ArrayList<Double>();
Все три объявления допустимы, так как и Integer, и Double наследуется от Number. В любом из трех случаев мы сможем получать из list'а переменную ссылочного типа на Number и вызывать методы этого класса. Другое дело, чтобы в первом случае по этой ссылке будут лежать Number и его наследники, во втором Ineger и его наследники, в третьем Double и его наследники. А теперь, как вы думаете, что мы можем записать в list с таким объявлением? Если вы ответили — Number и его любого наследника, то вы ошиблись. Ответ — ничего! Причина этому в том, что объявленный так лист на самом деле может быть как листом Number, так и листом Integer, Double, да и вообще любого наследника Number и поэтому неизвестно какой именно тип там храниться и что именно туда можно записать. Рассмотрим ситуацию с ключевым словом super.
List<? super Integer> list = new ArrayList<Integer>();
List<? super Integer> list = new ArrayList<Number>();
List<? super Integer> list = new ArrayList<Object>();
В такой ситуации все обстоит с точностью наоборот. Мы можем без приведения типов присовить значение из листа только ссылочной переменной типа Object, но зато запись в лист доступна для всех наследников типа Integer.
2)
List<Number> list = new ArrayList<Integer>().
Нет? Почему?!
Иногда, особенно, когда используешь стороннюю библиотеку (у меня такое часто возникало с библиотекой JasperReports), руки так и чешутся сделать подобное присвоение. И когда компилятор отказывается такое собирать, сразу же нахлынывает праведное негодование. В чем проблема? Почему? Как же полиморфизм?! Ведь кажется, в чем собственно проблема? В переменную-ссылку на лист Number записывается лист Integer, при этом Integer является прямым наследниками от типа Number и все его методы ему доступны, следовательно при получения элемента из коллекции проблем возникнуть не должно. Но они возникают, и если вы внимательно читали про ключевое слово super, то уже должны были понять почему.
Суть такова — лист Number и лист Integer это все-таки разные объекты. Лист Number подразумевает запись в него Number (а следовательно и Double, Float и прочего), чего, конечно, лист Integer делать не должен.
Но как говорится, если сильно хочется, то можно того, чего и нельзя!
List<Number> list = (List)new ArrayList<Integer>();
То есть, мы просто затерли информацию о типе изначального листа, получив так называемый класс с «сырым» типом, который уже можно присвоить к чему угодно. Проделывать такой трюк не рекомендуется.
3) Generic и Spring (или почему нужно вайрить по интерфейсу!)
Дошли до самого интересного. Данная проблема не связана напрямую с обобщенными типами, но вызвана с тем стилем, который они открывают. Рассмотрим несколько классов.
@MappedSuperclass
public abstract class BaseLinkEntity<S extends BaseEntity, T extends BaseEntity> extends BaseAuditableEntity {
@JoinColumn(name = "SOURCE_ID", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
protected S source;
@JoinColumn(name = "TARGET_ID", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
protected T target;
}
public interface LinkService<L extends BaseLinkEntity>{
List<L> getAllBySourceId(UUID id);
List<L> getAllByTargetId(UUID id);
}
public abstract class BaseLinkService<L extends BaseLinkEntity> implements LinkReportService<L> {
protected BaseLinkRepository<L> linkRepository;
@Required
public void setLinkRepository(BaseLinkRepository<L> linkRepository) {
this.linkRepository = linkRepository;
}
@Override
@Transactional(readOnly = true)
public List<L> getAllBySourceId(UUID id) {
return linkRepository.findBySourceId(id);
}
@Override
@Transactional(readOnly = true)
public List<L> getAllByTargetId(UUID id) {
return linkRepository.findByTargetId(id);
}
}
public class CardLinkServiceImpl extends BaseLinkReportService<CardLink> {
@Override
@Autowired
@Qualifier("cardLinkReportRepository")
public void setLinkRepository(BaseLinkRepository<CardLink> linkRepository) {
super.setLinkRepository(linkRepository);
}
}
public class MyClass{
@Autowired
private LinkService<CardLink> cardLinkServiceImpl;
}
Почти стандартный подход спринга (кроме промежуточного базового класса) — связка интерфейс-реализация. Сделано было для удобства работы с таблицами в БД, в которых сущности связаны связью многие ко многим. И вроде все хорошо. Но тут мне понадобилось в CardLinkServiceImpl вкорячить пару специфичных методов для данных линков. Чтобы не плодить промежуточных интерфейсов изначально я добавил их прямо в CardLinkServiceImpl и решил в нужном месте вайрить его прямо по классу. Итог: бин в контейнере не был найден.
Немножко порывшись в интернете причина такого поведения была найдена. Во-первых, Spring 3 не умеет вайрить классы с generic, но в проекте использовался Spring 4ой версии. Вторая причина в том, что в спринг частенько создаются прокси для классов, которые он кидает себе в контейнер.
Спринг много где использует AOP — аспектно-ориентированное программирование. При таком подходе некоторая логика не реализуется напрямую в методе, а навешивается на него средствами aop'а в спринге. Для реализации этого подхода на низком уровне, спринг в рантайме меняет байт-код классов, добавляя в них нужную логику, при этом он создаёт прокси-объект, в котором затёрта информация о изначальном объекте, но остаётся информация о его интерфейсах.
В данном случаем aop используется тут для управления транзакциями (аннотация @Transactional). В итоге мне пришлось выносить специфичные методы в отдельный интерфейс и вайрить уже по нему.
4) Generic и Hibernate 5.2.1
А теперь проблема в Hibernate, чем-то похожая на описанную в третьем пункте, но больше выглядящую, как баг. Немножко кода:
public class Card{
@JoinColumn(name = "KIND_ID")
@ManyToOne(fetch = FetchType.LAZY)
protected DocumentKind documentKind; //наследуется от BaseEntity, внутри которого есть поле Id
}
public class CardLink extends BaseLinkEntity<Card, Card>{
}
@NoRepositoryBean
public interface BaseLinkRepository<T extends BaseLinkEntity> extends JpaRepository<T, UUID>, JpaSpecificationExecutor<T> {
Page<T> findBySourceId(UUID sourceId, Pageable pageRequest);
Page<T> findByTargetId(UUID targetId, Pageable pageRequest);
}
public interface CardLinkReportRepository extends BaseLinkRepository<CardLink>{
List<CardLink> findByTargetDocumentKindId(UUID documentKindId);
}
Спринг не сможет создать реализацию CardLinkReportRepository из-за того, что хибернейт не сможет найти свойство documentKind. Фактически он будет искать его не в том объекте. Чтобы понять что к чему, мне пришлось долго дебажить и ковыряться в исходниках хибернейта. Видно, что возможность работать с обобщенными типами в него закладывалась, но реализована была как-то кривовато. Я попытаюсь в двух словах объяснить суть, но чтобы лучше понять вы можете сами поисследовать класс MetamodelImpl (стоит обратить внимание на методы buildMetamodel(Iterator persistentClasses, Set mappedSuperclasses, SessionFactoryImplementor sessionFactory, boolean ignoreUnsupported) и buildEntityType(PersistentClass persistentClass, MetadataContext context)) и класс AttributeFactory (метод buildAttribute(AbstractManagedType ownerType, Property property)).
При построении метамодели хибернейт сначала заносит туда все классы (buildEntity), и только после этого записывает в entity их атрибуты (аналогия в классах — поля) в методе метод — buildAttribute. Дело в том, что сущность в метамодели для объекта BaseLinkEntity создаётся в единственном экземпляре и конкретный тип атрибута (generic поля) определяется всего один раз в методе buildAttribute. Потом же, когда метод JPA репозитория ищет поля documentKind, он ищет поле в классе, который проставился при построении атрибута. Сам тип берется из контекста с которым у меня уже не хватило сил разобраться (где и в какой момент времени он создаётся). Вот и получается, что в полях BaseLinkEntity мы искать можем, а вот в специфичном типе generic'а только для одной сущности из всего множества в приложении.
Самое интересное (и пока непонятное для меня), что если переписать так, то все заработает.
@Query("SELECT link FROM CardLink link WHERE link.target.documentKind.id = ?1")
public interface CardLinkReportRepository extends BaseLinkRepository<CardLink>{
List<CardLink> findByTargetDocumentKindId(UUID documentKindId);
}
Автор: kobaeugenea