我应该如何将 JpaRepository.findOne() 与 SpringBoot 一起使用?

Posted

技术标签:

【中文标题】我应该如何将 JpaRepository.findOne() 与 SpringBoot 一起使用?【英文标题】:How should I use JpaRepository.findOne() with SpringBoot? 【发布时间】:2019-05-31 16:53:21 【问题描述】:

我刚刚通过阅读Spring Boot in Action 一书开始学习 Spring Boot,并且我正在学习本书的示例,并尝试自己运行它们,但我有一个使用JpaRepository.findOne()的问题。

我已经在整个章节中寻找可能的不匹配项。但是,它只是不起作用。

该项目应该是一个简单的阅读列表。

代码如下:

读者@Entity:

package com.lixin.readinglist;

import org.springframework.data.annotation.Id;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.Entity;
import java.util.Collection;
import java.util.Collections;

/**
 * @author lixin
 */
@Entity
public class Reader implements UserDetails 

    private static final long serialVersionUID = 1L;

    @Id
    private String username;
    private String fullname;
    private String password;

    @Override
    public String getUsername() 
        return username;
    

    public void setUsername(String username) 
        this.username = username;
    

    public String getFullname() 
        return fullname;
    

    public void setFullname(String fullname) 
        this.fullname = fullname;
    

    @Override
    public String getPassword() 
        return password;
    

    public void setPassword(String password) 
        this.password = password;
    

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() 
        return Collections.singletonList(new SimpleGrantedAuthority("READER"));
    

    @Override
    public boolean isAccountNonExpired() 
        return true;
    

    @Override
    public boolean isAccountNonLocked() 
        return true;
    

    @Override
    public boolean isCredentialsNonExpired() 
        return true;
    

    @Override
    public boolean isEnabled() 
        return true;
    

Jpa 接口:

package com.lixin.readinglist;

import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @author lixin
 */
public interface ReaderRepository extends JpaRepository<Reader, String> 

安全配置:

package com.lixin.readinglist;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

/**
 * @author lixin
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter 

    private final ReaderRepository readerRepository;

    @Autowired
    public SecurityConfig(ReaderRepository readerRepository) 
        this.readerRepository = readerRepository;
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http
                .authorizeRequests()
                .antMatchers("/").access("hasRole('READER')")
                .antMatchers("/**").permitAll()
                .and()
                .formLogin()
                .loginPage("/login")
                .failureUrl("/login?error=true");
    

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth
                .userDetailsService((UserDetailsService) username -> readerRepository.findOne(username));
    

我一直收到这个错误:

Error:(40, 86) java: method findOne in interface org.springframework.data.repository.query.QueryByExampleExecutor<T> cannot be applied to given types;
  required: org.springframework.data.domain.Example<S>
  found: java.lang.String
  reason: cannot infer type-variable(s) S
    (argument mismatch; java.lang.String cannot be converted to org.springframework.data.domain.Example<S>)

【问题讨论】:

您可能正在阅读一本旧书。在 Spring Data 的最新版本中,findOne() 已重命名为 findById()。它返回一个 Optional,而不是 Reader 或 null。所以你需要修改代码。 还请注意,使用String 值作为主键通常是一种低效的选择,因为查找往往很慢。 LongUUID 通常更好。 最后,查看 [Semantic Versioning](semver.org) 规范很有用。您的指南是为 Spring Data 1 编写的,而您正在使用 Spring Data 2,并且在主要版本之间可能会删除 API 组件——在这种情况下,findOne 方法已从 CrudRepository 中删除并替换为类似但不是相同的findById @JBNizet 感谢您的评论。我已经测试了 getOne() 方法,它抛出了你所说的确切错误。我已经更新了我的答案。作为一名本科生,很荣幸有你纠正我的错误,带领我走上正确的道路。感谢您的时间。 【参考方案1】:

findOne() 定义为&lt;S extends T&gt; Optional&lt;S&gt; findOne(Example&lt;S&gt; example);。 这意味着在您的情况下,它接受 Example&lt;Reader&gt; 并返回 Optional&lt;Reader&gt;。 您将String 传递给它,这是错误的,您将其用作AuthenticationManagerBuilder.userDetailsService() 中的lambda return,这也是错误的 因为UserDetailsService 是一个接口函数,定义为

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

所以你需要返回一个UserDetails 实例而不是它的Optional 或者如果与用户名to be compliant with the javadoc 不匹配则抛出UsernameNotFoundException

返回:

完全填充的用户记录(从不为空)

投掷:

UsernameNotFoundException - 如果找不到用户或用户 没有GrantedAuthority

此外,您不需要使用findOne(),这是一个示例查询。按 ID 查询就足够了。

所以你可以写这样的东西:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception 
   auth.userDetailsService(username -> readerRepository.findById(username)
                                                       .orElseThrow( () -> new UsernameNotFoundException("user with username " + username + " not found"));


附带说明,getOne() 足够棘手,因为它依赖于延迟加载,在某些情况下可能会带来意外的惊喜。 JB Nizet 的评论很有趣。 所以我现在测试了。当 Spring Security 类访问实体(即isAccountNonLocked())时,JPA 会话仍未打开。 所以在任何情况下都会抛出LazyInitializationException(用户名是否正确):

org.hibernate.LazyInitializationException:无法初始化代理 - 没有会话 在 org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:155) 在 org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:268) 在 org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:73) 在 davidhxxx.example.angularsboot.model.db.User_$$_jvstd90_5.isAccountNonLocked(User_$$_jvstd90_5.java) 在 org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider$DefaultPreAuthenticationChecks.check(AbstractUserDetailsAuthenticationProvider.java:352) 在 org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:165)

This question 可能会让您感兴趣。

【讨论】:

这里肯定是个问题:getOne() 将在 UserDetails 上返回一个未初始化的代理,无论用户是否实际存在。所以这会违反合同,因为在这种情况下应该抛出 UsernameNotFoundException。 @JB Nizet 事实上无论如何它都失败了。我刚测试过。我因此更新。谢谢你的评论;) 正如@JBNizet 提到的。我发现用getOne()代替findOne()肯定会违反约定,时不时会产生一些不好的意外。我意识到 Nizet 的评论非常出色。我只是一个天真的学生,很荣幸能有你纠正我的错误,带领我走上正确的道路。我将编辑我的答案以纠正我所犯的幼稚错误。谢谢&amp;&amp;感谢!【参考方案2】:

正如其他人所说,在最新版本的 Spring Data 2.x 中,您应该使用 findById,而不是 findOne,在最新版本的 Spring Data 中使用 findOne(如果您是 Spring-Boot 2.x 的一部分)使用它)想要一个示例对象。我的猜测是您使用的这本书是在最近发布的 Spring 5 / Spring Boot 2 / Spring Data 2.x 之前编写的。

希望阅读迁移指南作为您的 [稍微过时] 书的参考会有所帮助:https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide

【讨论】:

【参考方案3】:

你可以使用findById,而不是findOne,findOne想要一个示例对象,你可以看here了解更多

【讨论】:

【参考方案4】:

方法 findOne 来自一个名为 QueryByExampleExecutor 的接口,JpaRepository 接口对其进行了扩展。而 QueryByExampleExecutor 用于 QBE(一种使用示例的查询)。在您的代码中,您不应该使用它,您可以使用 getOne 方法或 findById 方法,findById 继承自 CrudRepository 接口。

【讨论】:

【参考方案5】:

您可以使用getOne(),而不是findOne()。作者可能搞错了。

【讨论】:

其实我认为你不应该在这里使用getOne,可能我们也犯了同样的错误。请阅读问题下方 Mr.Nizet 制作的 cmets。【参考方案6】:

阅读@davidxxx 的answer 和@JB Nizet 的评论后

我发现我犯了一个可怕的错误,用getOne()代替findOne()的想法肯定会违反合同,时不时会产生一些不好的意外。

我意识到 Nizet 的评论非常出色。我只是一个天真的学生,很荣幸能有你纠正我的错误,带领我走上正确的道路。我将编辑我的答案以纠正我所犯的幼稚错误。谢谢(@JB Nizet &amp;&amp; @davidxxx)&amp;&amp; 谢谢!

解决方案:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception 
    auth.
            userDetailsService(username -> readerRepository.findById(username)
                    .orElseThrow(() -> new UsernameNotFoundException("user with username " + username + " not found")));

你可以找到原因here#其实是@davidxxx的答案。

【讨论】:

我一般不建议getOne()。它依赖于惰性引用,您可能会遇到不好的意外(这里不是问题,但很好)。您可以阅读我的回答 ***.com/a/47370947/270371 和更一般的帖子。此外,您必须遵守 loadUserByUsername() 的约定:返回完全初始化的 UserDetailUsernameNotFoundExceptionJpaRepository.getOne() 不保证这一点。 @davidxxx 感谢您的评论。我已经测试了getOne() 方法,它抛出了你所说的确切错误。我已经更新了我的答案。作为一名本科生,很荣幸有你纠正我的错误,带领我走上正确的道路。感谢您的时间。 这是一个非常好的编辑。在我们的答案中为帖子的下一位读者留下好的内容是一件非常好的事情 (+1)。

以上是关于我应该如何将 JpaRepository.findOne() 与 SpringBoot 一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

我应该如何命名将两个表映射在一起的表? [关闭]

我应该如何将多个图像添加到视图中?

我应该如何将 TextChangedListener 添加到 EditText 组件?

我应该如何将 Windows 用户 ID 存储在数据库中?

Eclipse RCP:我应该将模型对象保存在哪里以及它们如何与视图对话?

我应该如何将列表数据添加到网格