当加载 spring-boot 和 spring-data-jpa 时,Hibernate 无法加载 JPA 2.1 Converter

Posted

技术标签:

【中文标题】当加载 spring-boot 和 spring-data-jpa 时,Hibernate 无法加载 JPA 2.1 Converter【英文标题】:Hibernate fails to load JPA 2.1 Converter when loaded with spring-boot and spring-data-jpa 【发布时间】:2015-04-04 22:39:38 【问题描述】:

我有一个 UUID 的自定义转换器,可以将其转换为字符串而不是二进制文件:

package de.kaiserpfalzEdv.commons.jee.db;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.UUID;

@Converter(autoApply = true)
public class UUIDJPAConverter implements AttributeConverter<UUID, String> 
    @Override
    public String convertToDatabaseColumn(UUID attribute) 
        return attribute.toString();
    

    @Override
    public UUID convertToEntityAttribute(String dbData) 
        return UUID.fromString(dbData);
    

转换器(我还有其他一些用于时间/日期处理的转换器)驻留在库 .jar 文件中。

然后我在 .jar 文件中有实体。喜欢这个:

package de.kaiserpfalzEdv.office.core.security;

import de.kaiserpfalzEdv.commons.jee.db.OffsetDateTimeJPAConverter;
import de.kaiserpfalzEdv.commons.jee.db.UUIDJPAConverter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

@Entity
@Table(
        name = "tickets"
)
public class SecurityTicket implements Serializable 
    private final static ZoneId TIMEZONE = ZoneId.of("UTC");
    private final static long DEFAULT_TTL = 600L;
    private final static long DEFAULT_RENEWAL = 600L;

    @Id @NotNull
    @Column(name = "id_", length=50, nullable = false, updatable = false, unique = true)
    @Convert(converter = UUIDJPAConverter.class)
    private UUID id;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "account_id_", nullable = false, updatable = false, unique = true)
    private Account account;

    @Convert(converter = OffsetDateTimeJPAConverter.class)
    @Column(name = "created_", nullable = false, updatable = false)
    private OffsetDateTime created;

    @Convert(converter = OffsetDateTimeJPAConverter.class)
    @Column(name = "validity_", nullable = false, updatable = false)
    private OffsetDateTime validity;


    @Deprecated
    public SecurityTicket() 
    


    public SecurityTicket(@NotNull final Account account) 
        id = UUID.randomUUID();
        this.account = account;
        created = OffsetDateTime.now(TIMEZONE);
        validity = created.plusSeconds(DEFAULT_TTL);
    


    public void renew() 
        validity = OffsetDateTime.now(TIMEZONE).plusSeconds(DEFAULT_RENEWAL);
    

    public boolean isValid() 
        OffsetDateTime now = OffsetDateTime.now(TIMEZONE);

        System.out.println(validity.toString() + " is hopefully after " + now.toString());

        return validity.isAfter(now);
    

    public UUID getId() 
        return id;
    

    public OffsetDateTime getValidity() 
        return validity;
    

    public String getAccountName() 
        return account.getAccountName();
    

    public String getDisplayName() 
        return account.getDisplayName();
    

    public Set<String> getRoles() 
        HashSet<String> result = new HashSet<>();

        account.getRoles().forEach(t -> result.add(t.getDisplayNumber()));

        return Collections.unmodifiableSet(result);
    

    public Set<String> getEntitlements() 
        return Collections.unmodifiableSet(new HashSet<>());
    


    @Override
    public boolean equals(Object obj) 
        if (obj == null) 
            return false;
        
        if (obj == this) 
            return true;
        
        if (obj.getClass() != getClass()) 
            return false;
        
        SecurityTicket rhs = (SecurityTicket) obj;
        return new EqualsBuilder()
                .append(this.id, rhs.id)
                .isEquals();
    

    @Override
    public int hashCode() 
        return new HashCodeBuilder()
                .append(id)
                .toHashCode();
    


    @Override
    public String toString() 
        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
                .append("id", id)

                .append("account", account)
                .append("validity", validity)
                .toString();
    

通过 maven 和 testng 运行集成测试时,数据库运行良好。但是当我启动应用程序(第三个 .jar 文件)时,我得到了一个令人讨厌的异常,归结为:

Caused by: org.hibernate.HibernateException: Wrong column type in kpoffice.tickets for column id_. Found: varchar, expected: binary(50)
        at org.hibernate.mapping.Table.validateColumns(Table.java:372)
        at org.hibernate.cfg.Configuration.validateSchema(Configuration.java:1338)
        at org.hibernate.tool.hbm2ddl.SchemaValidator.validate(SchemaValidator.java:175)
        at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:525)
        at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1859)
        at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl$4.perform(EntityManagerFactoryBuilderImpl.java:852)
        at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl$4.perform(EntityManagerFactoryBuilderImpl.java:845)
        at org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl.withTccl(ClassLoaderServiceImpl.java:398)
        at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:844)
        at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:60)
        at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:343)
        at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:318)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1625)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1562)
        ... 120 more

转换的自动应用不起作用。我试图将转换器注释到类和属性本身。但是没有使用转换器。但是,当我通过休眠特定注释添加休眠 UUID 类型时,休眠抱怨它不能具有同一属性的转换器和休眠类型定义。所以hibernate会读取转换器配置。

使用 envers 时,JPA 2.1 转换器不起作用。但我不在我的软件中使用 envers。

我希望有人知道我做错了什么......

【问题讨论】:

JPA 规范明确指出转换不会应用于实体的 id 属性。也许您可以删除您的 JPA 转换注释并改用特定于 Hibernate 的方法尝试它? ***.com/questions/39547615/… 【参考方案1】:

Andy Wilkinson gave the correct answer.阅读规范有很多帮助。

JPA 2.1 转换器不适用于@Id 带注释的属性。

谢谢安迪。

【讨论】:

JSR-000338 JavaTM Persistence 2.1 Chapter 11.1.10 Convert Annotation,Convert注解不应该用于指定以下的转换:Id attributes(包括embedded的属性ids 和派生的身份)、版本属性、关系属性和 根据nailedtothex.org/roller/kyle/entry/…,转换器应该使用EmbeededId。但是,就我而言,它也失败了:***.com/questions/48188365/… 有什么建议吗? 如果您阅读规范(由 Tom 引用),您会看到规范不包含嵌入式 id 的属性。因此,如果它有效,那是您不应该指望的专有扩展。【参考方案2】:

另一种选择是将转换逻辑嵌入替代 getter/setter 中,如下所示:

public class SecurityTicket implements Serializable

...
private UUID id;

@Transient
public UUID getUUID()

    return id;


@Id @NotNull
@Column(name = "id_", length=50, nullable = false, updatable = false, unique = true)
public String getID()

    return id.toString();


public void setUUID( UUID id )

    this.id = id;


public void setID( String id )

    this.id = UUID.fromString( id );


...

@Transient 注释将告诉 JPA 忽略此 getter,因此它认为没有单独的 UUID 属性。它不优雅,但它对我使用 JPA 对 UUID 作为 PK 的类有用。您确实冒着其他代码通过 setId(String) 方法设置错误值的风险,但这似乎是唯一的解决方法。这种方法有可能被保护/私有吗?

虽然普通的 Java 代码能够根据不同的参数类型区分同名的 setter,但如果您不以不同的方式命名它们,JPA 会报错。

令人讨厌的是,JPA 不支持 Id 上的转换器,或者它不遵循 JAXB 约定,即对于具有标准转换方法(即 toString/fromString、intValue/parseInt 等)的类不需要转换器。

【讨论】:

以上是关于当加载 spring-boot 和 spring-data-jpa 时,Hibernate 无法加载 JPA 2.1 Converter的主要内容,如果未能解决你的问题,请参考以下文章

Apache Ignite 使用 Spring-Boot 加载两次?

Spring-boot,thymeleaf 加载上传的图片,无需重启应用服务器

如何在 IDEA Intellij 上使用 Spring-boot 进行自动重新加载

从 json 文件加载 spring-boot 属性

使用 Gradle、Groovy 和 spring-boot 加载失败并出现 java.lang.NoSuchFieldException:classCache

Spring-Boot 多模块项目加载属性文件