简体   繁体   中英

How should I use JpaRepository.findOne() with SpringBoot?

I just started learning Spring Boot by reading the book Spring Boot in Action and I am learning the examples of this book, trying to run them myself but I have a problem using JpaRepository.findOne() .

I've gone allover the Chapter to find my possible mismatches. However, it just DO NOT work.

The project is supposed to be a simple Reading List.

Here is the code :

The Reader @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;
    }
}

The Jpa interface:

package com.lixin.readinglist;

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

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

The SecurityConfig:

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));
    }
}

And I kept getting this ERROR:

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>)

findOne() is defined as <S extends T> Optional<S> findOne(Example<S> example); .
It means that in your case it accepts a Example<Reader> and returns an Optional<Reader> .
You passed to it a String , which is wrong and you use it as lambda return in AuthenticationManagerBuilder.userDetailsService() , which is also wrong because UserDetailsService is an interface functional defined as

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

So you need to return an UserDetails instance not an Optional of it or to throw UsernameNotFoundException if no matching with the username to be compliant with the javadoc :

Returns:

a fully populated user record (never null)

Throws:

UsernameNotFoundException - if the user could not be found or the user has no GrantedAuthority

Besides you don't need to use findOne() that is a query by example. A query by ID is enough.

So you could write something like that :

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

As a side note, getOne() is tricky enough as it relies on lazy loading that may give bad surprises in some cases.
The remark of JB Nizet was interesting. So I tested right now. It happens that the JPA session is not still opened when the entity (namely isAccountNonLocked() ) is accessed by the Spring Security classes.
So a LazyInitializationException is thrown in any case (username correct or no) :

org.hibernate.LazyInitializationException: could not initialize proxy - no Session
        at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:155)
        at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:268)
        at org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:73)
        at davidhxxx.example.angularsboot.model.db.User_$$_jvstd90_5.isAccountNonLocked(User_$$_jvstd90_5.java)
        at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider$DefaultPreAuthenticationChecks.check(AbstractUserDetailsAuthenticationProvider.java:352)
        at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:165)

This question may interest you.

After reading the answer from @davidxxx and the comment from @JB Nizet

I found that I've made a terrible mistake, the idea of using getOne() to take the place of findOne() will definitely violate the contract and produce some bad surprises from time to time.

And I realized that Nizet's comment was beyond excellent. I'm just a naive student, and it's an honor to have you correct my mistake and lead me to the correct way. I gonna edit my answer to correct the naive mistakes I've made. Thank you (@JB Nizet && @davidxxx) && Appreciate that!

The Solution:

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

And you can find the reason here # Actually it is the answer from @davidxxx.

As others have said, in the latest versions of Spring Data 2.x, you should use findById, instead of findOne, findOne in the latest version of Spring Data (that is part of Spring-Boot 2.x if you are using that) wants an example object. My guess is that the book you were using was written before the recent release of Spring 5 / Spring Boot 2 / Spring Data 2.x.

Hopefully reading the migration guide as a reference alongside your [slightly out-of-date] book will help: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide

You can use getOne() , instead of findOne() . The author might have made a mistake.

您可以使用findById而不是findOne,findOne想要一个示例对象,您可以在这里查看更多内容

The method findOne is from an interface named QueryByExampleExecutor, the JpaRepository interface extended it. while QueryByExampleExecutor is used for QBE(a kind of query which uses Example). In your code, you shouldn't use it, you can use the getOne method or findById method, the findById is inherited from the CrudRepository interface.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM