Spring boot JPA:Unknown entity 解决方法

前言

JPA Unknown entity 其实是一个简单的问题,可因为基础不牢导致该问题的解决变得很耗时,特在此记录以示警示。下面表述的内容可能有不当之处,如有幸被各位看到,还望批评指正,非常感谢!

问题

最近在学习shiro权限管理的时使用JPA来做单表的增删改查,发现出现java.lang.IllegalArgumentException: Unknown entity:***.***.***Entity,排查了很久也没发现问题,最后因为是类的实例化问题。

答案

我的遇到的问题和解决方法如下。

/**
 * 用户注册
 *
 * @param account
 * @return
 */
1  @GetMapping("/register")
2  public RestfulModel ajaxRegister(@Valid Account account, HttpServletRequest request) throws Exception {
3      if (!Optional.ofNullable(account).map(Account::getUsername).isPresent()
4              || !Optional.ofNullable(account).map(Account::getPassword).isPresent()) {
5          throw new Exception("账号或密码不能为空!");
6      }
7      // 生成盐
8      String salt = new SecureRandomNumberGenerator().nextBytes().toHex();
9      //将原始密码加盐(上面生成的盐),并且用md5算法加密2次,将最后结果存入库中
10     String password = new Md5Hash(account.getPassword(), account.getCredentialsSalt(), 2).toString();
11     accountService.save(new Account(){{
12         setSalt(salt);
13         setPassword(password);
14         setLastLoginIp(IpUtil.getIpAddr(request));
15         setLastLoginTime(new Date());
16     }});
17     return new RestfulModel() {{
18         setStatus("success");
19     }};
20 }

报错出现在第11行,当代码执行到11行开始报错,说unkonw entity,其实改成下面这样就可以了。

/**
 * 用户注册
 *
 * @param account
 * @return
 */
1  @GetMapping("/register")
2  public RestfulModel ajaxRegister(@Valid Account account, HttpServletRequest request) throws Exception {
3      if (!Optional.ofNullable(account).map(Account::getUsername).isPresent()
4              || !Optional.ofNullable(account).map(Account::getPassword).isPresent()) {
5          throw new Exception("账号或密码不能为空!");
6      }
7      // 生成盐
8      String salt = new SecureRandomNumberGenerator().nextBytes().toHex();
9      //将原始密码加盐(上面生成的盐),并且用md5算法加密2次,将最后结果存入数据库中
10     String password = new Md5Hash(account.getPassword(), account.getCredentialsSalt(), 2).toString();
11     account.setSalt(salt);
12     account.setPassword(password);
13     account.setLastLoginIp(IpUtil.getIpAddr(request));
14     account.setLastLoginTime(new Date());
15     accountService.save(account);
16     return new RestfulModel() {{
17         setStatus("success");
18     }};
19 }

总结

如果你遇到的问题与我的不一样,请看:
① 请使用javax.persistence.Entity为你的实体类添加@Entity注解;
② 你可以尝试在YouProjectNameApplication.java入口注解处添加强制扫描Entity注解@EntityScan( basePackages = {"your.packagename.xxxEntity"}
③ 请采用上述原始的set方法,为你的Entity属性赋值,不要使用匿名类的方式。

解析

  • JPA在做 save操作时,放入的实体对象必须有@Entity注解,而采用匿名类的set方式导致JPA不能正确识别实体,所以会出现Unknown entity的报错。

那么问题来了:
为何采用上述的匿名内部类{{ set(); }}方法不能达到预期效果?原始的set方法与匿名内部类中的set方法有何不同?

  • 关于上述这个问题,我们有必要去了解一下Java中匿名内部类。这个问题纠结了很长时间,查看了很多博客又问了下公司的大牛最后又看了下java编程思想(第四版) / 第10章 内部类 / 10.6 匿名内部类才大概捋清楚为何JPA不能识别的问题。以下为个人理解,如有不当还请留言指正!

首先先定义个User实体类,以便后面使用:

public class User {

    private String email;
    private String something;
    // 省略getter、setter及toString
    ...
}
1. 普通类与匿名内部类的编译对比

普通类的编译一般只会产出一个.class文件:

public class Test {
    public static void main(String[] args) {
        User user = new User();
        user.setEmail("gofen2010@163.com");
        user.setSomething("Send email!");

        System.out.println(user.toString());
    }
}

编译后:


普通Test类编译后的.class文件

匿名内部类会产出两个以上的.class文件:

// Test.java
public class Test {
    public static void main(String[] args) {
        User user = new User(){{
            setEmail("gofen2010@163.com");
            setSomething("Send email!");
        }};

        System.out.println(user.toString());
    }
}

编译后:

包含匿名内部类的Test类编译后的.class文件

我们发现两种方式编译后产出的文件数量不一致,采用匿名内部类的Test.class要比上面多一个。

2. 普通类与匿名内部类编译后的.class内容对比

普通类.class内容:

// Test.class
public class Test {
    public Test() {
    }

    public static void main(String[] args) {
        User user = new User();
        user.setEmail("gofen2010@163.com");
        user.setSomething("Send email!");
        System.out.println(user.toString());
    }
}

匿名内部类的.class内容:

// Test.class
public class Test {
    public Test() {
    }

    public static void main(String[] args) {
        User user = new User() {
            {
                this.setEmail("gofen2010@163.com");
                this.setSomething("Send email!");
            }
        };
        System.out.println(user.toString());
    }
}
// Test$1.class
final class Test$1 extends User {
    Test$1() {
        this.setEmail("gofen2010@163.com");
        this.setSomething("Send email!");
    }
}

带有匿名内部类的Test.java类在编译后,会多出一个继承自UserTest$1.class,而如果你看完java编程思想(第四版) / 第10章 内部类 / 10.6 匿名内部类,你会发现带有匿名内部类的Test.java可以写成下面这样。

3. 改造带有匿名内部类的Test.java

我们可以这样写:

// Test.java
public class Test {
    public static void main(String[] args) {
        User user = new t1();
        System.out.println(user.toString());
    }

    static class t1 extends User {
        t1() {
            this.setEmail("gofen2010@163.com");
            this.setSomething("Send email");
        }
    }
}

我想看到这大家应该就明白了,为何采用{{ set(); }}的方式就不能被JPA识别了。假设User.java类添加了@Entity注解,但是继承Usert1类并没有添加@Entity注解,故而无法被JPA识别。

当然大家可能还有一些疑问:
① 给t1添加@Entity注解不就行了么?
当然可以,我们采用这个{{ set(); }}方式来写的目的,就是为了代码更简单一些,但如果采用问题中描述的那样来做,其实也就没多大必要了。
② 难道我们不可以继承父类的注解吗?
目前@Entity不可以继承。原因是在Spring boot的public @interface Entity{...}注解类中并没有@Inherited,故不可继承。至于为何要这样还未深入研究。