如何基于实体接口声明存储库



在一个新项目中,我们希望使用Spring Data JPA并为所有JPA实体定义接口,例如:

public interface Person extends Serializable {
  void setId(Long id);
  Long getId();
  void setLastName(String lastName);
  String getLastName();
  void setFirstName(String firstName);
  String getFirstName();
  // ...
}
@Entity
@Table(name = "t_persons")
public class PersonEntity implements Person {
  private static final long serialVersionUID = 1L;
  @Id
  @Column
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  @Column
  private String firstName;
  @Column
  private String lastName;
  // ...
}

但是,当基于像

这样的接口声明Spring Data存储库时
public interface PersonRepository extends JpaRepository<Person, Long> {
}

Spring上下文初始化失败,原因是

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'personRepository': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not an managed type: interface com.example.Person
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1513)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:293)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:290)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:191)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:917)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:860)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:775)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:489)
    ... 24 more
Caused by: java.lang.IllegalArgumentException: Not an managed type: interface com.example.Person
    at org.hibernate.ejb.metamodel.MetamodelImpl.managedType(MetamodelImpl.java:171)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.<init>(JpaMetamodelEntityInformation.java:70)
    at org.springframework.data.jpa.repository.support.JpaEntityInformationSupport.getMetadata(JpaEntityInformationSupport.java:65)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getEntityInformation(JpaRepositoryFactory.java:146)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:84)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:67)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:150)
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.initAndReturn(RepositoryFactoryBeanSupport.java:224)
    at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:210)
    at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:84)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1572)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1510)
    ... 34 more

我没有发现任何仓库依赖于接口而不是具体类型的例子,所以这是可能的吗?如果是,怎么做?

看起来,如果我们不能使用接口来声明存储库,那么使用这些接口将非常困难,因为我们将在服务中到处使用显式强制转换,甚至在处理泛型(List, Iterable…)时使用未检查的强制转换。

这是解决你问题的办法。我不知道为什么Spring的人决定把他们的存储库建立在具体的类上。但至少他们让改变这一切成为可能。

  1. 您需要在EnableJpaRepositories中提供自定义repositoryFactoryBeanClass,例如:
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
 * @author goraczka
 */
@EnableJpaRepositories(
    repositoryFactoryBeanClass = InterfaceBasedJpaRepositoryFactoryBean.class
)
public class DatabaseConfig {
}
  • 那么你需要实现InterfaceBasedJpaRepositoryFactoryBean。它是一个Spring钩子,可以为存储库bean创建自定义工厂。
    import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
    import org.springframework.data.repository.Repository;
    import org.springframework.data.repository.core.support.RepositoryFactorySupport;
    import javax.persistence.EntityManager;
    /**
     * @author goraczka
     */
    public class InterfaceBasedJpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
            extends JpaRepositoryFactoryBean<T, S, ID> {
        public InterfaceBasedJpaRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
            super(repositoryInterface);
        }
        protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
            return new InterfaceBasedJpaRepositoryFactory(entityManager);
        }
    }
    
  • 最后但并非最不重要的是自定义存储库bean工厂,它试图将存储库本身定义的接口与EntityManager上注册的实体类相匹配。
  • import org.springframework.data.jpa.repository.support.JpaEntityInformation;
    import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
    import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
    import org.springframework.util.Assert;
    import javax.persistence.EntityManager;
    import java.util.AbstractMap;
    import java.util.Arrays;
    import java.util.Map;
    import java.util.stream.Collectors;
    /**
     * @author goraczka
     */
    public class InterfaceBasedJpaRepositoryFactory extends JpaRepositoryFactory {
        private final Map<? extends Class<?>, ? extends Class<?>> interfaceToEntityClassMap;
        private final EntityManager entityManager;
        public InterfaceBasedJpaRepositoryFactory(EntityManager entityManager) {
            super(entityManager);
            this.entityManager = entityManager;
            interfaceToEntityClassMap = entityManager
                    .getMetamodel()
                    .getEntities()
                    .stream()
                    .flatMap(et -> Arrays.stream(et.getJavaType().getInterfaces())
                            .map(it -> new AbstractMap.SimpleImmutableEntry<>(it, et.getJavaType())))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (possibleDuplicateInterface, v) -> v));
        }
        @Override
        @SuppressWarnings("unchecked")
        public <T, ID> JpaEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
            Assert.isTrue(domainClass.isInterface(), "You are using interface based jpa repository support. " +
                    "The entity type used in DAO should be an interface");
            Class<T> domainInterface = domainClass;
            Class<?> entityClass = interfaceToEntityClassMap.get(domainInterface);
            Assert.notNull(entityClass, "Entity class for a interface" + domainInterface + " not found!");
            return (JpaEntityInformation<T, ID>) JpaEntityInformationSupport.getEntityInformation(entityClass, entityManager);
        }
    }
    
    不要因为我的任何错误而责备我。在看完这个问题后,我在10分钟内完成了这个问题,并意识到还没有解决方案。我真的需要一个。我没有为它创建任何测试,但似乎工作。我们欢迎改进。

    我遇到了同样的问题,并使用@NoRepositoryBean在使用接口而不是具体类的存储库接口上解决了这个问题(感谢那篇博客文章):

    import org.springframework.data.repository.NoRepositoryBean;
    @NoRepositoryBean
    public interface PersonRepository<P extends Person> extends JpaRepository<P, Long> {
        // code all generic methods using fields in Person interface
    }
    

    然后,使用扩展另一个的具体存储库:

    public interface PersonEntityRepository extends PersonRepository<PersonEntity> {
        // code all specific methods that use fields in PersonEntity class
    }
    

    此注释至少在spring-data-commons-2.1.9.RELEASE.jar中存在。

    在@goroncy的回复之后,我还必须重写方法getRepositoryMetadata

    @Override
    protected RepositoryMetadata getRepositoryMetadata(Class<?> repositoryInterface) {
        RepositoryMetadata ret = super.getRepositoryMetadata(repositoryInterface);
        Class<?> clazz = ret.getClass();
        try {
            Field f = clazz.getDeclaredField("domainType");
            boolean isAccessible = f.isAccessible();
            f.setAccessible(true);
            Class<?> actualValue = (Class<?>) f.get(ret);
            Class<?> newValue = this.interfaceToEntityClassMap.get(actualValue);
            f.set(ret, newValue);
            f.setAccessible(isAccessible);
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return ret;
    }
    

    我添加了对基于实体的存储库的兼容性支持,而不仅仅是接口。默认启用

    private boolean allowNonInterfaceTypes = true;
    

    可以通过调用InterfaceBasedJpaRepositoryFactoryBean

    中的2参数构造函数来禁用它
    @Override
    protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
        return new InterfaceBasedJpaRepositoryFactory(entityManager, false);
    }
    

    完整的类保持为

    import java.lang.reflect.Field;
    import java.util.AbstractMap;
    import java.util.Arrays;
    import java.util.Map;
    import java.util.stream.Collectors;
    import javax.persistence.EntityManager;
    import org.springframework.data.jpa.repository.support.JpaEntityInformation;
    import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
    import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
    import org.springframework.data.repository.core.RepositoryMetadata;
    import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
    import org.springframework.util.Assert;
    public class InterfaceBasedJpaRepositoryFactory extends JpaRepositoryFactory {
        private final Map<? extends Class<?>, ? extends Class<?>> interfaceToEntityClassMap;
        private final EntityManager entityManager;
        private boolean allowNonInterfaceTypes = true;
        public InterfaceBasedJpaRepositoryFactory(EntityManager entityManager) {
            super(entityManager);
            this.entityManager = entityManager;
            this.interfaceToEntityClassMap = entityManager.getMetamodel().getEntities().stream()
                    .flatMap(et -> Arrays.stream(et.getJavaType().getInterfaces())
                            .map(it -> new AbstractMap.SimpleImmutableEntry<>(it, et.getJavaType())))
                    .collect(
                            Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (possibleDuplicateInterface, v) -> v));
        }
        public InterfaceBasedJpaRepositoryFactory(EntityManager entityManager, boolean paramAllowNonInterfaceTypes) {
            this(entityManager);
            this.allowNonInterfaceTypes = paramAllowNonInterfaceTypes;
        }
        @Override
        @SuppressWarnings("unchecked")
        public <T, ID> JpaEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
            JpaEntityInformation<T, ID> ret = null;
            // Interface entities
            if (this.allowNonInterfaceTypes == false) {
                Assert.isTrue(domainClass.isInterface(),
                        "You are using interface based jpa repository support. The entity type used in DAO should be an interface");
                Class<T> domainInterface = domainClass;
                Class<?> entityClass = this.interfaceToEntityClassMap.get(domainInterface);
                Assert.notNull(entityClass, String.format("Entity class for a interface %s not found!", domainInterface));
                ret = (JpaEntityInformation<T, ID>) JpaEntityInformationSupport.getEntityInformation(entityClass, entityManager);
            }else {
                // Class entities
                ret = super.getEntityInformation(domainClass);
            }
            return ret;
        }
        @Override
        protected RepositoryMetadata getRepositoryMetadata(Class<?> repositoryInterface) {
            RepositoryMetadata ret = super.getRepositoryMetadata(repositoryInterface);
            Class<?> clazz = ret.getClass();
            try {
                Field f = clazz.getDeclaredField("domainType");
                boolean isAccessible = f.isAccessible();
                f.setAccessible(true);
                Class<?> actualValue = (Class<?>) f.get(ret);
                Class<?> newValue = this.interfaceToEntityClassMap.get(actualValue);
                f.set(ret, newValue);
                f.setAccessible(isAccessible);
            } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
            }
            return ret;
        }
    }
    

    它为我工作与spring-data-jpa 2.0.8。RELEASE来自spring-boot-start -data-jpa 2.0.3.RELEASE

    如果有什么错误,我很抱歉,我很久以前就做过了。

    Person接口缺少@Entity注释,因此不能识别为托管对象。我认为将@Entity注释放在Person接口上也没有帮助,因为该注释不是继承的。我认为你应该忘记Person接口,或者在存储库声明中使用PersonEntity。没有实际检查代码-所以非常抱歉,如果这个答案是错误的…

    最新更新