Bug
A NullPointerException occurs at startup when an @Entity class extends a generic @MappedSuperclass through two or more levels of inheritance (e.g. Concrete extends Middle<Long> / Middle<T> extends Base<T>).
Stack trace
Caused by: java.lang.NullPointerException: Cannot invoke "java.lang.Class.isEnum()"
because the return value of "io.ebeaninternal.server.deploy.meta.DeployBeanProperty.getPropertyType()" is null
at io.ebeaninternal.server.deploy.parse.AnnotationFields.readField(AnnotationFields.java:121)
at io.ebeaninternal.server.deploy.parse.AnnotationFields.parse(AnnotationFields.java:62)
at io.ebeaninternal.server.deploy.parse.ReadAnnotations.readInitial(ReadAnnotations.java:29)
Reproducer
@MappedSuperclass
public abstract class GenericBaseModel<T> extends Model {
@Id T id;
// ...
}
@MappedSuperclass
public abstract class GenericMiddleModel<T> extends GenericBaseModel<T> {
// intermediate layer
}
@Entity
@Table(name = "middle_product")
public class ProductWithGenericMiddle extends GenericMiddleModel<Long> {
String name;
}
Ebean fails to start with the NPE above. The single-level case (Concrete extends GenericBaseModel<Long> directly) works correctly.
Root cause
Introduced in ebc90e0. DeployCreateProperties.mapGenerics(Class) built the TypeVariable → Class map by reading only the direct generic superclass of each class as the recursion walked up the hierarchy. For a chain like:
ProductWithGenericMiddle extends GenericMiddleModel<Long>
GenericMiddleModel<T> extends GenericBaseModel<T>
GenericBaseModel<T> { @Id T id; }
When the recursion reached GenericBaseModel, it called mapGenerics(GenericMiddleModel). GenericMiddleModel.getGenericSuperclass() is GenericBaseModel<T> - where T is still an unresolved TypeVariable. resolveToClass(TypeVariable) returned null, so the map was empty. genericTypeMap.get(id.getGenericType()) then returned null, leaving propertyType = null. This null was stored in the DeployBeanProperty and caused the NPE in AnnotationFields.
Fix
Build the full type-variable map once for the concrete bean type using TypeResolver.getTypeVariableMap, which walks the entire superclass/interface hierarchy and composes TypeVariable bindings transitively (Middle.T → Long then Base.T → resolve(Middle.T) → Long). The same map is reused at every level of the recursive createProperties walk, so any TypeVariable at any depth in the hierarchy resolves correctly.
Additionally:
- The map type is widened from
Map<TypeVariable<?>, Class<?>> to Map<TypeVariable<?>, Type> so intermediate ParameterizedType values (e.g. List<T>) are preserved and resolved transitively rather than being discarded.
- A null-safety fallback (
propertyType = field.getType() when resolution yields null) prevents the NPE even for unresolvable type variables.
- Type-resolution helpers (
resolveType, resolveToClass, resolveCollectionTarget) are consolidated in TypeReflectHelper for reuse.
Bug
A
NullPointerExceptionoccurs at startup when an@Entityclass extends a generic@MappedSuperclassthrough two or more levels of inheritance (e.g.Concrete extends Middle<Long>/Middle<T> extends Base<T>).Stack trace
Reproducer
Ebean fails to start with the NPE above. The single-level case (
Concrete extends GenericBaseModel<Long>directly) works correctly.Root cause
Introduced in ebc90e0.
DeployCreateProperties.mapGenerics(Class)built theTypeVariable → Classmap by reading only the direct generic superclass of each class as the recursion walked up the hierarchy. For a chain like:When the recursion reached
GenericBaseModel, it calledmapGenerics(GenericMiddleModel).GenericMiddleModel.getGenericSuperclass()isGenericBaseModel<T>- whereTis still an unresolvedTypeVariable.resolveToClass(TypeVariable)returnednull, so the map was empty.genericTypeMap.get(id.getGenericType())then returnednull, leavingpropertyType = null. This null was stored in theDeployBeanPropertyand caused the NPE inAnnotationFields.Fix
Build the full type-variable map once for the concrete bean type using
TypeResolver.getTypeVariableMap, which walks the entire superclass/interface hierarchy and composesTypeVariablebindings transitively (Middle.T → LongthenBase.T → resolve(Middle.T) → Long). The same map is reused at every level of the recursivecreatePropertieswalk, so anyTypeVariableat any depth in the hierarchy resolves correctly.Additionally:
Map<TypeVariable<?>, Class<?>>toMap<TypeVariable<?>, Type>so intermediateParameterizedTypevalues (e.g.List<T>) are preserved and resolved transitively rather than being discarded.propertyType = field.getType()when resolution yields null) prevents the NPE even for unresolvable type variables.resolveType,resolveToClass,resolveCollectionTarget) are consolidated inTypeReflectHelperfor reuse.